OAuth Security Best Practices
Comprehensive OAuth 2.0 security guide with best practices, vulnerability prevention, and secure implementation patterns for developers and security teams.
Understanding OAuth 2.0 Security
OAuth 2.0 is a powerful authorization framework that enables applications to obtain limited access to user accounts. However, improper implementation can lead to serious security vulnerabilities. This guide covers essential security practices for developers implementing OAuth 2.0.
๐ Authorization vs Authentication
OAuth 2.0 is for authorization (what can you do), not authentication (who are you). Don't confuse it with authentication protocols like OIDC.
๐ก๏ธ Common Attack Vectors
CSRF, authorization code interception, token injection, and redirect URI manipulation are the most common OAuth vulnerabilities to protect against.
โ Defense in Depth
Use PKCE, validate all parameters, implement proper state handling, and secure token storage for comprehensive protection.
OAuth 2.0 vs OAuth 1.0
| Feature | OAuth 1.0 | OAuth 2.0 |
|---|---|---|
| Transport Security | Built-in encryption | Requires HTTPS |
| Complexity | High (crypto signatures) | Lower (bearer tokens) |
| Token Types | Single token type | Access + Refresh tokens |
| Mobile Support | Limited | Excellent |
| Status | Legacy (don't use for new projects) | Current standard |
๐ฏ Recommendation: Always use OAuth 2.0 for new projects. OAuth 1.0 is deprecated and should only be used when interacting with legacy systems that don't support OAuth 2.0.
Choosing the Right OAuth Flow
โ Authorization Code Flow (Recommended)
Most secure flow for web applications with a backend. Always use with PKCE for additional security.
- Tokens never exposed to browser/user
- Supports refresh tokens for long-lived access
- Required for confidential clients
- Use with PKCE even for confidential clients
Common OAuth Security Vulnerabilities
๐ฏ Authorization Code Interception
Attack: Malicious apps intercept authorization codes through custom URL schemes or compromised redirect URIs.
- Always use PKCE (Proof Key for Code Exchange)
- Validate redirect URIs strictly
- Use https:// redirect URIs, never custom schemes
- Implement short-lived authorization codes (10 minutes max)
๐ CSRF Attacks
Attack: Attackers trick users into completing OAuth flows with attacker-controlled accounts.
- Always use and validate the state parameter
- Make state unguessable (cryptographically random)
- Bind state to user session
- Implement CSRF tokens in addition to state
๐ Token Injection
Attack: Attackers inject stolen access tokens into victim's application session.
- Bind tokens to client identity (token binding)
- Use sender-constrained tokens (mTLS, DPoP)
- Validate token audience and issuer
- Implement proper session management
Secure Implementation Examples
Secure Authorization Code Flow with PKCE
class SecureOAuth {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.authEndpoint = config.authEndpoint;
this.tokenEndpoint = config.tokenEndpoint;
}
// Generate PKCE challenge
async generatePKCE() {
// Generate code verifier (43-128 characters)
const codeVerifier = this.base64URLEncode(
crypto.getRandomValues(new Uint8Array(32))
);
// Generate code challenge
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = this.base64URLEncode(new Uint8Array(digest));
return { codeVerifier, codeChallenge };
}
// Start authorization flow
async startAuthorization(scopes) {
// Generate PKCE and state
const { codeVerifier, codeChallenge } = await this.generatePKCE();
const state = this.base64URLEncode(crypto.getRandomValues(new Uint8Array(16)));
// Store for later verification
sessionStorage.setItem('oauth_code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: scopes.join(' '),
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
// Redirect to authorization server
window.location.href = `${this.authEndpoint}?${params.toString()}`;
}
// Handle authorization callback
async handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
// Check for errors
if (error) {
throw new Error(`OAuth error: ${error}`);
}
// Validate state parameter (CSRF protection)
const storedState = sessionStorage.getItem('oauth_state');
if (!state || state !== storedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
// Get stored code verifier
const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
if (!codeVerifier) {
throw new Error('Missing code verifier');
}
// Exchange code for tokens
const tokens = await this.exchangeCodeForTokens(code, codeVerifier);
// Clean up storage
sessionStorage.removeItem('oauth_code_verifier');
sessionStorage.removeItem('oauth_state');
return tokens;
}
// Exchange authorization code for tokens
async exchangeCodeForTokens(code, codeVerifier) {
const response = await fetch(this.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
code: code,
redirect_uri: this.redirectUri,
code_verifier: codeVerifier
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
const tokens = await response.json();
// Validate token response
if (!tokens.access_token) {
throw new Error('Invalid token response');
}
return tokens;
}
base64URLEncode(buffer) {
const bytes = new Uint8Array(buffer);
const base64 = btoa(String.fromCharCode.apply(null, bytes));
return base64.replace(/+/g, '-').replace(///g, '_').replace(/=/g, '');
}
}
// Usage
const oauth = new SecureOAuth({
clientId: 'your-client-id',
redirectUri: 'https://yourapp.com/callback',
authEndpoint: 'https://auth.provider.com/oauth/authorize',
tokenEndpoint: 'https://auth.provider.com/oauth/token'
});
// Start authorization
oauth.startAuthorization(['read', 'write']);
// Handle callback (call this on redirect page)
oauth.handleCallback().then(tokens => {
console.log('Access token:', tokens.access_token);
}).catch(error => {
console.error('OAuth error:', error);
});Secure Token Storage
class SecureTokenStorage {
constructor() {
this.storageKey = 'oauth_tokens';
}
// Store tokens securely
storeTokens(tokens) {
// For web apps - use httpOnly cookies when possible
// This example shows secure client-side storage
const tokenData = {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + (tokens.expires_in * 1000),
tokenType: tokens.token_type || 'Bearer'
};
// Store in secure httpOnly cookie (server-side)
this.setSecureCookie('access_token', tokens.access_token, tokens.expires_in);
// Or use sessionStorage for temporary storage (less secure)
sessionStorage.setItem(this.storageKey, JSON.stringify(tokenData));
}
// Get valid access token
async getAccessToken() {
// Try to get from cookie first
let token = this.getCookie('access_token');
if (!token) {
// Fallback to sessionStorage
const stored = sessionStorage.getItem(this.storageKey);
if (!stored) return null;
const tokenData = JSON.parse(stored);
// Check if token is expired
if (Date.now() >= tokenData.expiresAt) {
// Try to refresh token
const refreshed = await this.refreshAccessToken(tokenData.refreshToken);
if (refreshed) {
this.storeTokens(refreshed);
return refreshed.access_token;
}
return null;
}
token = tokenData.accessToken;
}
return token;
}
// Refresh access token
async refreshAccessToken(refreshToken) {
if (!refreshToken) return null;
try {
const response = await fetch('/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: 'your-client-id'
})
});
if (!response.ok) {
this.clearTokens();
return null;
}
return await response.json();
} catch (error) {
console.error('Token refresh failed:', error);
this.clearTokens();
return null;
}
}
// Make authenticated API request
async apiRequest(url, options = {}) {
const token = await this.getAccessToken();
if (!token) {
throw new Error('No valid access token available');
}
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
};
const response = await fetch(url, {
...options,
headers
});
// Handle token expiration
if (response.status === 401) {
this.clearTokens();
throw new Error('Token expired, please re-authenticate');
}
return response;
}
// Clear all stored tokens
clearTokens() {
sessionStorage.removeItem(this.storageKey);
this.deleteCookie('access_token');
}
// Helper methods for cookie management
setSecureCookie(name, value, maxAge) {
// This should be done server-side for httpOnly cookies
const cookie = `${name}=${value}; Max-Age=${maxAge}; Secure; SameSite=Strict; Path=/`;
document.cookie = cookie;
}
getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
}
// Usage
const tokenStorage = new SecureTokenStorage();
// Store tokens after OAuth flow
tokenStorage.storeTokens(oauthTokens);
// Make authenticated requests
tokenStorage.apiRequest('/api/user/profile').then(response => {
return response.json();
}).then(profile => {
console.log('User profile:', profile);
}).catch(error => {
console.error('API request failed:', error);
});Token Storage Security
| Storage Method | Security | XSS Vulnerable | CSRF Vulnerable | Recommendation |
|---|---|---|---|---|
| httpOnly Cookie | High | No | Yes* | โ Recommended |
| Memory (JS variable) | Medium | Yes | No | โ ๏ธ Short-lived only |
| sessionStorage | Medium | Yes | No | โ ๏ธ Temporary use |
| localStorage | Low | Yes | No | โ Avoid |
* CSRF Protection: Use SameSite=Strict cookies and implement additional CSRF tokens for state-changing operations. httpOnly cookies with proper CSRF protection provide the best security for most applications.
Real-World OAuth Implementations
๐ GitHub OAuth
Flow: Authorization Code + PKCE
Scopes: Granular (repo, user, gist)
Security: Required app review for public apps
Best for: Developer tools, CI/CD systems
๐ Google OAuth
Flow: Authorization Code + PKCE
Scopes: Very granular per-API
Security: Advanced protection program
Best for: Consumer apps, GSuite integration
๐ผ Microsoft OAuth
Flow: Authorization Code + PKCE
Scopes: Microsoft Graph API
Security: Conditional access policies
Best for: Enterprise apps, Office 365
๐ต Spotify OAuth
Flow: Authorization Code + PKCE
Scopes: User data, playlist modification
Security: Rate limiting, quota management
Best for: Music apps, analytics tools
OAuth Security Checklist
โ Client-Side Security
- โ Always use HTTPS for OAuth endpoints
- โ Implement PKCE for all OAuth flows
- โ Validate state parameter to prevent CSRF
- โ Use short-lived access tokens (1 hour max)
- โ Store tokens in httpOnly cookies when possible
- โ Implement proper error handling
- โ Use Content Security Policy (CSP)
- โ Validate redirect URIs strictly
๐ง Server-Side Security
- โ Validate all OAuth parameters
- โ Implement rate limiting on OAuth endpoints
- โ Use refresh token rotation
- โ Log all OAuth events for monitoring
- โ Implement token introspection
- โ Use mTLS for sensitive applications
- โ Set appropriate token scopes
- โ Implement token binding where possible