Home / Notebooks / Security
Security
intermediate

OAuth Essentials

Essential OAuth and OpenID Connect concepts for secure authentication and authorization

March 10, 2024
•
Updated regularly

OAuth Essentials

Quick reference guide for OAuth 2.0 and OpenID Connect fundamentals.

What is OAuth?

OAuth 2.0 is an authorization framework that:

  • Delegates access without sharing passwords
  • Used by Google, Facebook, GitHub, etc.
  • Provides scoped access to resources
  • Secure token-based authentication
  • Industry standard for API authorization
  • Not for authentication (use OpenID Connect)
  • Key Concepts

    Roles

    ┌──────────┐                                  ┌──────────────┐
    │ Resource │                                  │ Authorization│
    │  Owner   │                                  │    Server    │
    │ (User)   │                                  │              │
    └──────────┘                                  └──────────────┘
         │                                               │
         │ 1. Authorization Request                      │
         │──────────────────────────────────────────────►│
         │                                               │
         │ 2. Authorization Grant                        │
         │◄──────────────────────────────────────────────│
         │                                               │
    ┌──────────┐                                  ┌──────────────┐
    │  Client  │                                  │   Resource   │
    │Application│                                 │    Server    │
    │          │                                  │     (API)    │
    └──────────┘                                  └──────────────┘
         │                                               │
         │ 3. Access Token Request                       │
         │──────────────────────────────────────────────►│
         │                                               │
         │ 4. Access Token                               │
         │◄──────────────────────────────────────────────│
         │                                               │
         │ 5. Access Resource                            │
         │──────────────────────────────────────────────►│
         │                                               │
         │ 6. Protected Resource                         │
         │◄──────────────────────────────────────────────│
    
  • Resource Owner: User who owns the data
  • Client: Application requesting access
  • Authorization Server: Issues access tokens
  • Resource Server: API hosting protected resources
  • Tokens

  • Access Token: Credentials to access resources (short-lived)
  • Refresh Token: Used to obtain new access tokens (long-lived)
  • ID Token: Claims about user identity (OpenID Connect)
  • Scopes

    Define the level of access:

    read:user          # Read user profile
    write:repo         # Write to repositories
    admin:org          # Admin organization access
    openid profile     # OpenID Connect scopes
    

    OAuth 2.0 Flows

    1. Authorization Code Flow (Most Secure)

    Use Case: Server-side web applications

    User                 Client              Auth Server         Resource Server
     |                     |                      |                     |
     |   1. Login Request  |                      |                     |
     |-------------------->|                      |                     |
     |                     |                      |                     |
     |   2. Redirect to Auth                      |                     |
     |                     |--------------------->|                     |
     |                     |                      |                     |
     |   3. Login & Consent|                      |                     |
     |<--------------------------------------------|                     |
     |                     |                      |                     |
     |   4. Auth Code      |                      |                     |
     |-------------------->|<---------------------|                     |
     |                     |                      |                     |
     |   5. Exchange Code for Token               |                     |
     |                     |--------------------->|                     |
     |                     |   6. Access Token    |                     |
     |                     |<---------------------|                     |
     |                     |                      |                     |
     |   7. Access Resource                       |                     |
     |                     |------------------------------------->|     |
     |                     |   8. Protected Resource             |     |
     |                     |<-------------------------------------|     |
    

    Implementation Example (Node.js):

    const express = require('express');
    const axios = require('axios');
    
    const app = express();
    
    const config = {
      clientId: 'your-client-id',
      clientSecret: 'your-client-secret',
      redirectUri: 'http://localhost:3000/callback',
      authorizationUrl: 'https://oauth.provider.com/authorize',
      tokenUrl: 'https://oauth.provider.com/token',
      scope: 'read:user'
    };
    
    // Step 1: Redirect to authorization server
    app.get('/login', (req, res) => {
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scope,
        state: generateRandomState() // CSRF protection
      });
      
      res.redirect(`${config.authorizationUrl}?${params}`);
    });
    
    // Step 2: Handle callback with authorization code
    app.get('/callback', async (req, res) => {
      const { code, state } = req.query;
      
      // Verify state parameter (CSRF protection)
      if (!verifyState(state)) {
        return res.status(400).send('Invalid state');
      }
      
      try {
        // Step 3: Exchange code for access token
        const response = await axios.post(config.tokenUrl, {
          grant_type: 'authorization_code',
          code,
          client_id: config.clientId,
          client_secret: config.clientSecret,
          redirect_uri: config.redirectUri
        });
        
        const { access_token, refresh_token, expires_in } = response.data;
        
        // Store tokens securely (session, database, etc.)
        req.session.accessToken = access_token;
        req.session.refreshToken = refresh_token;
        
        res.redirect('/profile');
      } catch (error) {
        res.status(500).send('Authentication failed');
      }
    });
    
    // Use access token to access protected resources
    app.get('/profile', async (req, res) => {
      const accessToken = req.session.accessToken;
      
      if (!accessToken) {
        return res.redirect('/login');
      }
      
      try {
        const response = await axios.get('https://api.provider.com/user', {
          headers: {
            'Authorization': `Bearer ${accessToken}`
          }
        });
        
        res.json(response.data);
      } catch (error) {
        if (error.response?.status === 401) {
          // Token expired, refresh it
          return refreshAccessToken(req, res);
        }
        res.status(500).send('Error fetching profile');
      }
    });
    
    // Refresh access token
    async function refreshAccessToken(req, res) {
      const refreshToken = req.session.refreshToken;
      
      try {
        const response = await axios.post(config.tokenUrl, {
          grant_type: 'refresh_token',
          refresh_token: refreshToken,
          client_id: config.clientId,
          client_secret: config.clientSecret
        });
        
        req.session.accessToken = response.data.access_token;
        res.redirect('/profile');
      } catch (error) {
        res.redirect('/login');
      }
    }
    

    2. Authorization Code Flow with PKCE

    Use Case: Mobile and single-page applications (no client secret)

    const crypto = require('crypto');
    
    // Generate code verifier and challenge
    function generatePKCE() {
      const codeVerifier = crypto.randomBytes(32).toString('base64url');
      const codeChallenge = crypto
        .createHash('sha256')
        .update(codeVerifier)
        .digest('base64url');
      
      return { codeVerifier, codeChallenge };
    }
    
    // Step 1: Authorization request with PKCE
    app.get('/login-pkce', (req, res) => {
      const { codeVerifier, codeChallenge } = generatePKCE();
      
      // Store code verifier for later use
      req.session.codeVerifier = codeVerifier;
      
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scope,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256',
        state: generateRandomState()
      });
      
      res.redirect(`${config.authorizationUrl}?${params}`);
    });
    
    // Step 2: Exchange code with code verifier
    app.get('/callback-pkce', async (req, res) => {
      const { code } = req.query;
      const codeVerifier = req.session.codeVerifier;
      
      try {
        const response = await axios.post(config.tokenUrl, {
          grant_type: 'authorization_code',
          code,
          client_id: config.clientId,
          redirect_uri: config.redirectUri,
          code_verifier: codeVerifier
          // No client_secret needed with PKCE
        });
        
        const { access_token } = response.data;
        req.session.accessToken = access_token;
        
        res.redirect('/profile');
      } catch (error) {
        res.status(500).send('Authentication failed');
      }
    });
    

    3. Client Credentials Flow

    Use Case: Machine-to-machine (M2M) communication

    // No user involved, direct token request
    async function getClientCredentialsToken() {
      try {
        const response = await axios.post(config.tokenUrl, {
          grant_type: 'client_credentials',
          client_id: config.clientId,
          client_secret: config.clientSecret,
          scope: 'api:read api:write'
        }, {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        });
        
        return response.data.access_token;
      } catch (error) {
        console.error('Failed to get access token:', error);
        throw error;
      }
    }
    
    // Use token for API requests
    async function callAPI() {
      const token = await getClientCredentialsToken();
      
      const response = await axios.get('https://api.example.com/data', {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      });
      
      return response.data;
    }
    

    4. Implicit Flow (Deprecated)

    Not recommended - use Authorization Code Flow with PKCE instead

    // ❌ BAD: Implicit flow (deprecated)
    app.get('/login-implicit', (req, res) => {
      const params = new URLSearchParams({
        response_type: 'token', // Returns token in URL fragment
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scope
      });
      
      res.redirect(`${config.authorizationUrl}?${params}`);
    });
    
    // Token exposed in URL - security risk!
    

    5. Resource Owner Password Credentials (Legacy)

    Not recommended - requires user to share password with client

    // ❌ BAD: Password credentials (legacy)
    async function loginWithPassword(username, password) {
      const response = await axios.post(config.tokenUrl, {
        grant_type: 'password',
        username,
        password,
        client_id: config.clientId,
        client_secret: config.clientSecret
      });
      
      return response.data.access_token;
    }
    

    OpenID Connect (OIDC)

    OAuth 2.0 + Authentication Layer

    ID Token (JWT)

    // ID Token structure
    {
      "header": {
        "alg": "RS256",
        "kid": "key-id"
      },
      "payload": {
        "iss": "https://accounts.google.com",
        "sub": "user-unique-id",
        "aud": "your-client-id",
        "exp": 1234567890,
        "iat": 1234567800,
        "email": "user@example.com",
        "email_verified": true,
        "name": "John Doe",
        "picture": "https://example.com/photo.jpg"
      },
      "signature": "..."
    }
    

    Verify ID Token

    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      jwksUri: 'https://oauth.provider.com/.well-known/jwks.json'
    });
    
    function getKey(header, callback) {
      client.getSigningKey(header.kid, (err, key) => {
        const signingKey = key.publicKey || key.rsaPublicKey;
        callback(null, signingKey);
      });
    }
    
    function verifyIdToken(idToken) {
      return new Promise((resolve, reject) => {
        jwt.verify(idToken, getKey, {
          algorithms: ['RS256'],
          audience: config.clientId,
          issuer: 'https://oauth.provider.com'
        }, (err, decoded) => {
          if (err) {
            reject(err);
          } else {
            resolve(decoded);
          }
        });
      });
    }
    
    // Usage
    app.get('/callback-oidc', async (req, res) => {
      const { code } = req.query;
      
      // Exchange code for tokens
      const response = await axios.post(config.tokenUrl, {
        grant_type: 'authorization_code',
        code,
        client_id: config.clientId,
        client_secret: config.clientSecret,
        redirect_uri: config.redirectUri
      });
      
      const { access_token, id_token } = response.data;
      
      // Verify ID token
      try {
        const claims = await verifyIdToken(id_token);
        console.log('User:', claims);
        
        req.session.user = claims;
        res.redirect('/dashboard');
      } catch (error) {
        res.status(401).send('Invalid ID token');
      }
    });
    

    UserInfo Endpoint

    async function getUserInfo(accessToken) {
      const response = await axios.get('https://oauth.provider.com/userinfo', {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      });
      
      return response.data;
    }
    

    Provider-Specific Examples

    Google OAuth

    const config = {
      clientId: 'your-client-id.apps.googleusercontent.com',
      clientSecret: 'your-client-secret',
      redirectUri: 'http://localhost:3000/callback',
      authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
      tokenUrl: 'https://oauth2.googleapis.com/token',
      scope: 'openid profile email'
    };
    
    app.get('/login/google', (req, res) => {
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scope,
        access_type: 'offline', // Get refresh token
        prompt: 'consent'
      });
      
      res.redirect(`${config.authorizationUrl}?${params}`);
    });
    

    GitHub OAuth

    const config = {
      clientId: 'github-client-id',
      clientSecret: 'github-client-secret',
      redirectUri: 'http://localhost:3000/callback/github',
      authorizationUrl: 'https://github.com/login/oauth/authorize',
      tokenUrl: 'https://github.com/login/oauth/access_token',
      scope: 'read:user user:email'
    };
    
    app.get('/login/github', (req, res) => {
      const params = new URLSearchParams({
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scope,
        state: generateRandomState()
      });
      
      res.redirect(`${config.authorizationUrl}?${params}`);
    });
    
    app.get('/callback/github', async (req, res) => {
      const { code } = req.query;
      
      const response = await axios.post(config.tokenUrl, {
        client_id: config.clientId,
        client_secret: config.clientSecret,
        code,
        redirect_uri: config.redirectUri
      }, {
        headers: {
          'Accept': 'application/json'
        }
      });
      
      const { access_token } = response.data;
      
      // Get user info
      const userResponse = await axios.get('https://api.github.com/user', {
        headers: {
          'Authorization': `Bearer ${access_token}`
        }
      });
      
      req.session.user = userResponse.data;
      res.redirect('/dashboard');
    });
    

    Facebook OAuth

    const config = {
      clientId: 'facebook-app-id',
      clientSecret: 'facebook-app-secret',
      redirectUri: 'http://localhost:3000/callback/facebook',
      authorizationUrl: 'https://www.facebook.com/v18.0/dialog/oauth',
      tokenUrl: 'https://graph.facebook.com/v18.0/oauth/access_token',
      scope: 'email public_profile'
    };
    

    Security Best Practices

    1. State Parameter (CSRF Protection)

    const crypto = require('crypto');
    
    function generateState() {
      const state = crypto.randomBytes(32).toString('hex');
      return state;
    }
    
    app.get('/login', (req, res) => {
      const state = generateState();
      req.session.oauthState = state;
      
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: config.clientId,
        redirect_uri: config.redirectUri,
        scope: config.scope,
        state // CSRF token
      });
      
      res.redirect(`${config.authorizationUrl}?${params}`);
    });
    
    app.get('/callback', (req, res) => {
      const { state } = req.query;
      
      // Verify state parameter
      if (state !== req.session.oauthState) {
        return res.status(400).send('Invalid state - CSRF attack detected');
      }
      
      delete req.session.oauthState;
      // Continue with token exchange...
    });
    

    2. Secure Token Storage

    // ❌ BAD: Store in localStorage (XSS vulnerable)
    localStorage.setItem('access_token', token);
    
    // ✅ GOOD: Store in httpOnly cookie (backend only)
    res.cookie('access_token', token, {
      httpOnly: true,
      secure: true, // HTTPS only
      sameSite: 'strict',
      maxAge: 3600000 // 1 hour
    });
    
    // ✅ GOOD: Store in session (server-side)
    req.session.accessToken = token;
    
    // ✅ GOOD: Store in encrypted database
    await db.tokens.create({
      userId: user.id,
      accessToken: encrypt(token),
      expiresAt: new Date(Date.now() + 3600000)
    });
    

    3. Token Validation

    async function validateAccessToken(token) {
      // Introspection endpoint
      const response = await axios.post('https://oauth.provider.com/introspect', {
        token,
        client_id: config.clientId,
        client_secret: config.clientSecret
      });
      
      return response.data.active === true;
    }
    
    // Middleware
    async function requireAuth(req, res, next) {
      const token = req.session.accessToken;
      
      if (!token) {
        return res.status(401).send('Unauthorized');
      }
      
      const isValid = await validateAccessToken(token);
      
      if (!isValid) {
        return res.status(401).send('Invalid token');
      }
      
      next();
    }
    
    app.get('/protected', requireAuth, (req, res) => {
      res.json({ message: 'Protected resource' });
    });
    

    4. Scope Validation

    function hasScope(requiredScopes) {
      return (req, res, next) => {
        const tokenScopes = req.session.scopes || [];
        
        const hasAllScopes = requiredScopes.every(scope => 
          tokenScopes.includes(scope)
        );
        
        if (!hasAllScopes) {
          return res.status(403).send('Insufficient permissions');
        }
        
        next();
      };
    }
    
    app.get('/admin', requireAuth, hasScope(['admin:read']), (req, res) => {
      res.json({ message: 'Admin resource' });
    });
    

    5. Token Expiration

    function isTokenExpired(token) {
      const decoded = jwt.decode(token);
      
      if (!decoded || !decoded.exp) {
        return true;
      }
      
      const now = Math.floor(Date.now() / 1000);
      return decoded.exp < now;
    }
    
    async function getValidToken(req) {
      let token = req.session.accessToken;
      
      if (isTokenExpired(token)) {
        // Refresh token
        token = await refreshAccessToken(req.session.refreshToken);
        req.session.accessToken = token;
      }
      
      return token;
    }
    

    Common Vulnerabilities

    1. Authorization Code Interception

    Risk: Attacker intercepts authorization code

    Mitigation: Use PKCE

    // Use PKCE for mobile and SPA apps
    const { codeVerifier, codeChallenge } = generatePKCE();
    

    2. Redirect URI Manipulation

    Risk: Attacker redirects to malicious URL

    Mitigation: Strict redirect URI validation

    const ALLOWED_REDIRECTS = [
      'http://localhost:3000/callback',
      'https://myapp.com/callback'
    ];
    
    function validateRedirectUri(uri) {
      return ALLOWED_REDIRECTS.includes(uri);
    }
    

    3. Token Leakage

    Risk: Tokens exposed in logs, URLs, browser history

    Mitigation: Never expose tokens in URLs

    // ❌ BAD: Token in URL
    res.redirect(`/dashboard?token=${accessToken}`);
    
    // ✅ GOOD: Token in secure cookie or session
    req.session.accessToken = accessToken;
    res.redirect('/dashboard');
    

    Testing OAuth

    Mock OAuth Server

    // For testing purposes
    app.get('/mock/authorize', (req, res) => {
      const { redirect_uri, state } = req.query;
      const code = 'mock-auth-code';
      res.redirect(`${redirect_uri}?code=${code}&state=${state}`);
    });
    
    app.post('/mock/token', (req, res) => {
      res.json({
        access_token: 'mock-access-token',
        token_type: 'Bearer',
        expires_in: 3600,
        refresh_token: 'mock-refresh-token'
      });
    });
    

    Tips

  • Always use HTTPS in production
  • Never expose client secret on frontend
  • Use PKCE for mobile and SPA apps
  • Implement state parameter for CSRF protection
  • Store tokens securely (never in localStorage)
  • Validate all tokens before use
  • Use short-lived access tokens with refresh tokens
  • Implement token rotation for refresh tokens
  • Log OAuth events for security monitoring
  • Handle token expiration gracefully
  • OAuth vs OpenID Connect

    FeatureOAuth 2.0OpenID Connect
    PurposeAuthorizationAuthentication + Authorization
    TokenAccess TokenAccess Token + ID Token
    User InfoNo standardUserInfo endpoint
    Use CaseAPI accessUser login
    ScopeCustom scopesopenid scope required

    Resources

  • OAuth 2.0 Specification
  • OpenID Connect
  • OAuth 2.0 Security Best Practices
  • PKCE RFC 7636
  • JWT Handbook
  • Topics

    OAuthAuthenticationSecurityAPIAuthorization

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.