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.