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

FeatureOAuth 1.0OAuth 2.0
Transport SecurityBuilt-in encryptionRequires HTTPS
ComplexityHigh (crypto signatures)Lower (bearer tokens)
Token TypesSingle token typeAccess + Refresh tokens
Mobile SupportLimitedExcellent
StatusLegacy (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.

Prevention:
  • 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.

Prevention:
  • 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.

Prevention:
  • 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

oauth-pkce.js
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

token-storage.js
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 MethodSecurityXSS VulnerableCSRF VulnerableRecommendation
httpOnly CookieHighNoYes*โœ… Recommended
Memory (JS variable)MediumYesNoโš ๏ธ Short-lived only
sessionStorageMediumYesNoโš ๏ธ Temporary use
localStorageLowYesNoโŒ 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

Related Security Tools

Security Reminder: OAuth security is critical for protecting user data and preventing unauthorized access. Always use the latest OAuth 2.1 recommendations, implement PKCE, validate all parameters, and consider using OpenID Connect for authentication. Regular security audits and penetration testing are essential for OAuth implementations in production.