Security Best Practices
This guide provides comprehensive security best practices for implementing OAuth 2.0 with WeTrials. Following these practices will help protect your application and users from common security vulnerabilities.
Core Security Principles
1. Always Use HTTPS
All OAuth communications must use HTTPS to prevent token interception:
// ✅ CORRECT - HTTPS everywhere
const AUTH_ENDPOINT = 'https://auth.wetrials.com/v1/oauth/authorize';
const TOKEN_ENDPOINT = 'https://auth.wetrials.com/v1/oauth/token';
const REDIRECT_URI = 'https://yourapp.com/callback';
// ❌ WRONG - Never use HTTP in production
const AUTH_ENDPOINT = 'http://auth.wetrials.com/v1/oauth/authorize';
// Exception: localhost for development only
const DEV_REDIRECT_URI = 'http://localhost:3000/callback';2. Implement PKCE for All Clients
Even confidential clients should use PKCE for additional security:
class SecureOAuth {
constructor() {
this.pkceRequired = true; // Always use PKCE
}
async startAuthorization() {
// Generate PKCE parameters for every authorization
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
// Store verifier securely
this.secureStorage.setItem('pkce_verifier', codeVerifier);
// Include in authorization request
const params = {
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
code_challenge: codeChallenge,
code_challenge_method: 'S256', // Always use S256, never plain
state: this.generateState(),
nonce: this.generateNonce(), // Additional replay protection
};
return this.buildAuthUrl(params);
}
generateCodeVerifier() {
// Minimum 43 characters, maximum 128
const length = 64;
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return this.base64URLEncode(array);
}
async generateCodeChallenge(verifier) {
// Always use SHA-256, never plain text
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return this.base64URLEncode(new Uint8Array(hash));
}
}3. Validate State Parameter
Always use and validate the state parameter to prevent CSRF attacks:
class StateManager {
generateState() {
// Generate cryptographically secure random state
const array = new Uint8Array(16);
crypto.getRandomValues(array);
const state = this.base64URLEncode(array);
// Store with expiration
const stateData = {
value: state,
createdAt: Date.now(),
expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
};
sessionStorage.setItem('oauth_state', JSON.stringify(stateData));
return state;
}
validateState(receivedState) {
const stored = sessionStorage.getItem('oauth_state');
if (!stored) {
throw new Error('No state found - possible CSRF attack');
}
const stateData = JSON.parse(stored);
// Check expiration
if (Date.now() > stateData.expiresAt) {
sessionStorage.removeItem('oauth_state');
throw new Error('State expired - please try again');
}
// Validate state matches
if (receivedState !== stateData.value) {
throw new Error('State mismatch - possible CSRF attack');
}
// Clear used state
sessionStorage.removeItem('oauth_state');
return true;
}
}Token Security
Secure Token Storage
Never Use localStorage for Sensitive Tokens
// ❌ WRONG - Vulnerable to XSS attacks
localStorage.setItem('access_token', token);
localStorage.setItem('refresh_token', refreshToken);
// ✅ BETTER - Use memory storage
class TokenStore {
constructor() {
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
}
setTokens(tokens) {
this.accessToken = tokens.access_token;
this.refreshToken = tokens.refresh_token;
this.expiresAt = Date.now() + tokens.expires_in * 1000;
}
getAccessToken() {
if (Date.now() >= this.expiresAt) {
return null; // Token expired
}
return this.accessToken;
}
clear() {
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
}
}
// ✅ BEST - Server-side session with httpOnly cookies
// Store tokens server-side, use secure session cookieServer-Side Token Storage
// Node.js with encrypted session storage
const session = require('express-session');
const MongoStore = require('connect-mongo');
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
crypto: {
secret: process.env.ENCRYPTION_KEY,
},
}),
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
})
);
// Store tokens in encrypted session
app.post('/auth/callback', async (req, res) => {
const tokens = await exchangeCodeForTokens(req.body.code);
// Encrypt tokens before storing
req.session.tokens = {
access: encrypt(tokens.access_token),
refresh: encrypt(tokens.refresh_token),
expiresAt: Date.now() + tokens.expires_in * 1000,
};
res.redirect('/dashboard');
});Token Validation
Always validate tokens before use:
class TokenValidator {
validateAccessToken(token) {
try {
// Decode JWT without verification (for client-side)
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid token format');
}
const payload = JSON.parse(atob(parts[1]));
// Check expiration
if (payload.exp * 1000 < Date.now()) {
throw new Error('Token expired');
}
// Check issuer
if (payload.iss !== 'https://auth.wetrials.com') {
throw new Error('Invalid issuer');
}
// Check audience
if (payload.aud !== this.clientId) {
throw new Error('Invalid audience');
}
return payload;
} catch (error) {
console.error('Token validation failed:', error);
return null;
}
}
// Server-side validation with signature verification
async validateTokenServerSide(token) {
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.wetrials.com/.well-known/jwks.json',
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
algorithms: ['RS256'],
issuer: 'https://auth.wetrials.com',
audience: this.clientId,
},
(err, decoded) => {
if (err) reject(err);
else resolve(decoded);
}
);
});
}
}Token Rotation
Implement automatic token rotation for enhanced security:
class TokenRotation {
constructor() {
this.rotationBuffer = 5 * 60 * 1000; // 5 minutes before expiry
}
async getValidToken() {
const token = this.tokenStore.getAccessToken();
const expiresAt = this.tokenStore.getExpiresAt();
// Check if token needs refresh
if (!token || Date.now() >= expiresAt - this.rotationBuffer) {
return await this.refreshToken();
}
return token;
}
async refreshToken() {
const refreshToken = this.tokenStore.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token available');
}
try {
const response = await fetch('/v1/oauth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId,
}),
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
// Update stored tokens
this.tokenStore.setTokens(tokens);
// Refresh tokens are rotated - update the stored refresh token
return tokens.access_token;
} catch (error) {
// Refresh failed - need new authorization
this.handleRefreshFailure();
throw error;
}
}
handleRefreshFailure() {
// Clear all tokens
this.tokenStore.clear();
// Redirect to login
window.location.href = '/login';
}
}Client Security
Protect Client Secrets
// ❌ WRONG - Never expose client secrets in client-side code
const CLIENT_SECRET = 'abc123_my_secret';
// ❌ WRONG - Don't commit secrets to version control
const config = {
clientId: 'my_client_id',
clientSecret: 'my_client_secret', // Never do this
};
// ✅ CORRECT - Server-side only, environment variables
// server.js
const config = {
clientId: process.env.WETRIALS_CLIENT_ID,
clientSecret: process.env.WETRIALS_CLIENT_SECRET,
};
// ✅ CORRECT - Use proxy endpoint for public clients
// Client-side makes request to your server
fetch('/api/oauth/token', {
method: 'POST',
body: JSON.stringify({ code, codeVerifier }),
});
// Server-side handles OAuth with secret
app.post('/api/oauth/token', async (req, res) => {
const { code, codeVerifier } = req.body;
const tokens = await exchangeCode(code, codeVerifier, CLIENT_SECRET);
req.session.tokens = tokens;
res.json({ success: true });
});Implement Content Security Policy
Protect against XSS attacks with proper CSP headers:
// Express.js example
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "https://auth.wetrials.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.wetrials.com", "https://auth.wetrials.com"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
sandbox: ['allow-forms', 'allow-scripts', 'allow-same-origin'],
reportUri: '/api/csp-report',
upgradeInsecureRequests: []
}
}));
// HTML meta tag alternative
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://auth.wetrials.com; connect-src 'self' https://api.wetrials.com">Network Security
Implement Request Signing
Add request signatures for additional security:
class SecureAPIClient {
async makeSignedRequest(endpoint, method, body) {
const timestamp = Date.now();
const nonce = crypto.randomUUID();
// Create signature
const signatureBase = `${method}${endpoint}${timestamp}${nonce}${JSON.stringify(body)}`;
const signature = await this.createHmacSignature(signatureBase);
const response = await fetch(endpoint, {
method,
headers: {
Authorization: `Bearer ${this.accessToken}`,
'X-Request-Timestamp': timestamp,
'X-Request-Nonce': nonce,
'X-Request-Signature': signature,
},
body: JSON.stringify(body),
});
return response;
}
async createHmacSignature(data) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey('raw', encoder.encode(this.clientSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
}
}Certificate Pinning
For mobile applications, implement certificate pinning:
// iOS Swift example
class PinnedSessionManager: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust,
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let serverCertData = SecCertificateCopyData(certificate) as Data
let localCertPath = Bundle.main.path(forResource: "wetrials", ofType: "cer")!
let localCertData = NSData(contentsOfFile: localCertPath)!
if serverCertData == localCertData as Data {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}Session Management
Implement Secure Session Handling
class SessionManager {
constructor() {
this.sessionTimeout = 30 * 60 * 1000; // 30 minutes
this.warningTime = 5 * 60 * 1000; // 5 minutes before timeout
this.lastActivity = Date.now();
}
startSessionMonitoring() {
// Monitor user activity
['click', 'keypress', 'scroll', 'mousemove'].forEach((event) => {
document.addEventListener(event, () => this.updateActivity());
});
// Check session periodically
setInterval(() => this.checkSession(), 60000); // Every minute
}
updateActivity() {
this.lastActivity = Date.now();
}
checkSession() {
const inactiveTime = Date.now() - this.lastActivity;
if (inactiveTime >= this.sessionTimeout) {
this.endSession('Session timeout due to inactivity');
} else if (inactiveTime >= this.sessionTimeout - this.warningTime) {
this.showWarning(this.sessionTimeout - inactiveTime);
}
}
endSession(reason) {
console.log('Ending session:', reason);
// Clear tokens
this.tokenStore.clear();
// Revoke tokens on server
this.revokeTokens();
// Clear session storage
sessionStorage.clear();
// Redirect to login
window.location.href = `/login?reason=${encodeURIComponent(reason)}`;
}
async revokeTokens() {
try {
await fetch('/v1/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: this.tokenStore.getRefreshToken(),
token_type_hint: 'refresh_token',
}),
});
} catch (error) {
console.error('Token revocation failed:', error);
}
}
}Concurrent Session Management
Prevent or manage concurrent sessions:
class ConcurrentSessionManager {
async validateSession() {
const sessionId = this.getSessionId();
const response = await fetch('/api/validate-session', {
method: 'POST',
headers: {
Authorization: `Bearer ${this.accessToken}`,
'X-Session-ID': sessionId,
},
});
if (!response.ok) {
const error = await response.json();
if (error.code === 'CONCURRENT_SESSION') {
this.handleConcurrentSession();
}
}
}
handleConcurrentSession() {
// Option 1: Force logout other sessions
if (confirm('You are logged in elsewhere. Force logout other sessions?')) {
this.forceLogoutOtherSessions();
} else {
// Option 2: Logout current session
this.logout();
}
}
}Audit and Monitoring
Implement Security Logging
class SecurityLogger {
logSecurityEvent(event) {
const logEntry = {
timestamp: new Date().toISOString(),
event: event.type,
userId: event.userId,
clientId: event.clientId,
ip: event.ip,
userAgent: event.userAgent,
success: event.success,
details: event.details,
};
// Send to logging service
this.sendToLoggingService(logEntry);
// Check for suspicious patterns
this.checkSuspiciousActivity(logEntry);
}
checkSuspiciousActivity(logEntry) {
// Multiple failed login attempts
if (logEntry.event === 'login_failed') {
this.trackFailedAttempts(logEntry);
}
// Unusual location or device
if (this.isUnusualAccess(logEntry)) {
this.alertSecurityTeam(logEntry);
}
// Rapid token refreshes
if (this.detectRapidRefreshes(logEntry)) {
this.flagPotentialAbuse(logEntry);
}
}
}Security Checklist
Development Phase
- [ ] Use HTTPS for all OAuth endpoints
- [ ] Implement PKCE for all authorization flows
- [ ] Validate state parameter on every callback
- [ ] Store tokens securely (never in localStorage)
- [ ] Implement token validation before use
- [ ] Set up CSP headers
- [ ] Use secure random generators for state/nonce
- [ ] Implement session timeout
- [ ] Add security logging
Deployment Phase
- [ ] Rotate client secrets
- [ ] Review and minimize requested scopes
- [ ] Set up monitoring and alerting
- [ ] Implement rate limiting
- [ ] Configure CORS properly
- [ ] Enable security headers (HSTS, X-Frame-Options, etc.)
- [ ] Implement request signing for sensitive operations
- [ ] Set up audit logging
- [ ] Document security procedures
Maintenance Phase
- [ ] Regularly rotate client secrets
- [ ] Monitor for suspicious activity
- [ ] Review audit logs
- [ ] Update dependencies for security patches
- [ ] Conduct security audits
- [ ] Test token revocation
- [ ] Verify backup authentication methods
- [ ] Update security documentation
- [ ] Train team on security best practices
Common Security Vulnerabilities
1. Authorization Code Injection
Prevention: Always use PKCE and validate state parameter
2. Token Leakage
Prevention: Use secure storage, HTTPS only, proper CSP
3. CSRF Attacks
Prevention: State parameter, SameSite cookies, CSRF tokens
4. Open Redirect
Prevention: Validate redirect URIs against whitelist
5. Token Replay
Prevention: Use nonce, implement token binding, short expiration
Next Steps
- Error Handling - Secure error handling
- Authorization Code Flow - Secure implementation
- Managing Applications - Application security
- Scopes and Permissions - Principle of least privilege