OAuth and OpenID Connect

Module 26: Advanced Backend & API Development

Introduction to OAuth 2.0

OAuth 2.0 is an authorization framework that enables third-party applications to obtain limited access to a user's account on a server. It's designed to work with HTTP and provides specific authorization flows for web applications, desktop applications, mobile phones, and IoT devices.

graph TD A[Resource Owner/User] -->|Authorizes Access| B[Client Application] B -->|Authorization Request| C[Authorization Server] C -->|Authorization Grant| B B -->|Token Request with Grant| C C -->|Access Token| B B -->|Access Token| D[Resource Server] D -->|Protected Resources| B style C fill:#f9f,stroke:#333,stroke-width:2px style D fill:#bbf,stroke:#333,stroke-width:2px

Think of OAuth like a valet key for your car. With a regular key, the valet can drive your car anywhere. But with a valet key, they can only drive your car but not access the trunk or glove compartment. Similarly, OAuth allows you to give third-party applications a "valet key" to your data without handing over your "master key" (your username and password).

Key Players in OAuth 2.0

OAuth 2.0 Authorization Flows

OAuth 2.0 defines several "grant types" or flows for different use cases. Let's explore the most common ones:

Authorization Code Flow

The most secure and common flow, ideal for server-side web applications.

sequenceDiagram participant User participant Client participant Auth Server participant Resource Server User->>Client: Use Application Client->>User: Redirect to Auth Server User->>Auth Server: Authenticate & Authorize Auth Server->>User: Redirect with Authorization Code User->>Client: Authorization Code Client->>Auth Server: Exchange Code for Tokens Auth Server->>Client: Access Token & Refresh Token Client->>Resource Server: Request with Access Token Resource Server->>Client: Protected Resource

This flow is like applying for a temporary building pass at a secure facility:

  1. You (the User) tell the receptionist (Client) you need access
  2. The receptionist sends you to security (Auth Server)
  3. Security verifies your ID and checks if you're allowed in
  4. Security gives you a temporary pass request slip (Auth Code)
  5. You bring this slip back to the receptionist
  6. The receptionist calls security to confirm the slip is legitimate
  7. Security issues a temporary building pass (Access Token)
  8. The receptionist uses this pass to escort you into the building (Resource Server)

// NodeJS implementation with Express and Passport
const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2').Strategy;
const session = require('express-session');

const app = express();

// Session setup
app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: false
}));

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((obj, done) => {
  done(null, obj);
});

// Configure OAuth 2.0 strategy
passport.use(new OAuth2Strategy({
    authorizationURL: 'https://authorization-server.com/auth',
    tokenURL: 'https://authorization-server.com/token',
    clientID: 'YOUR_CLIENT_ID',
    clientSecret: 'YOUR_CLIENT_SECRET',
    callbackURL: 'http://localhost:3000/auth/callback'
  },
  function(accessToken, refreshToken, profile, done) {
    // Store tokens and profile information
    return done(null, {
      accessToken,
      refreshToken,
      profile
    });
  }
));

// Routes
app.get('/', (req, res) => {
  if (req.isAuthenticated()) {
    res.send('Logged in! <a href="/logout">Logout</a>');
  } else {
    res.send('Not logged in. <a href="/auth">Login</a>');
  }
});

// Start authorization flow
app.get('/auth', passport.authenticate('oauth2'));

// Authorization callback
app.get('/auth/callback',
  passport.authenticate('oauth2', { failureRedirect: '/' }),
  (req, res) => {
    // Successful authentication
    res.redirect('/');
  }
);

// Access protected resource
app.get('/resource', (req, res) => {
  if (!req.isAuthenticated()) {
    return res.redirect('/');
  }
  
  // Use the access token to request data from the resource server
  fetch('https://resource-server.com/api/data', {
    headers: {
      'Authorization': `Bearer ${req.user.accessToken}`
    }
  })
  .then(response => response.json())
  .then(data => {
    res.json(data);
  })
  .catch(error => {
    res.status(500).json({ error: 'Failed to fetch resource' });
  });
});

app.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
            

Implicit Flow (Not Recommended for New Applications)

A simplified flow designed for public clients that can't securely store client secrets (like JavaScript applications). This flow is no longer recommended due to security concerns.

sequenceDiagram participant User participant Client participant Auth Server participant Resource Server User->>Client: Use Application Client->>User: Redirect to Auth Server User->>Auth Server: Authenticate & Authorize Auth Server->>User: Redirect with Access Token in Fragment User->>Client: Access Token Client->>Resource Server: Request with Access Token Resource Server->>Client: Protected Resource

This flow is like getting a visitor badge at an office that's printed immediately upon showing your ID - there's no intermediate verification step.

Client Credentials Flow

Used for server-to-server authentication where no user is involved.

sequenceDiagram participant Client participant Auth Server participant Resource Server Client->>Auth Server: Client ID & Secret Auth Server->>Client: Access Token Client->>Resource Server: Request with Access Token Resource Server->>Client: Protected Resource

This flow is similar to two systems exchanging API keys or service credentials - like one corporate system authenticating to another using its own identity rather than a user's.


// Client Credentials Flow with Node.js
const axios = require('axios');
const qs = require('querystring');

async function getSystemAccessToken() {
  try {
    const response = await axios.post('https://authorization-server.com/token', 
      qs.stringify({
        grant_type: 'client_credentials',
        client_id: 'SYSTEM_CLIENT_ID',
        client_secret: 'SYSTEM_CLIENT_SECRET',
        scope: 'read write'
      }), 
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    return response.data.access_token;
  } catch (error) {
    console.error('Error getting system access token:', error);
    throw error;
  }
}

async function fetchResourceAsSystem() {
  try {
    const accessToken = await getSystemAccessToken();
    
    const resourceResponse = await axios.get('https://resource-server.com/api/data', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });
    
    return resourceResponse.data;
  } catch (error) {
    console.error('Error fetching resource:', error);
    throw error;
  }
}

// Usage in an API service
app.get('/system-data', async (req, res) => {
  try {
    const data = await fetchResourceAsSystem();
    res.json(data);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch system data' });
  }
});
            

Resource Owner Password Credentials Flow

Used when there is a high degree of trust between the user and the client. The user provides their credentials directly to the client.

sequenceDiagram participant User participant Client participant Auth Server participant Resource Server User->>Client: Username & Password Client->>Auth Server: Username, Password, Client ID & Secret Auth Server->>Client: Access Token & Refresh Token Client->>Resource Server: Request with Access Token Resource Server->>Client: Protected Resource

This is like lending your actual house key to a trusted friend - they get full access, but only because you trust them completely.

This flow is only appropriate for first-party applications where the client is developed by the same organization that controls the authorization server.

Authorization Code Flow with PKCE

A secure extension to the Authorization Code flow for public clients (like mobile and single-page applications).

sequenceDiagram participant User participant Client participant Auth Server participant Resource Server Client->>Client: Generate Code Verifier & Challenge User->>Client: Use Application Client->>User: Redirect to Auth Server with Challenge User->>Auth Server: Authenticate & Authorize Auth Server->>User: Redirect with Authorization Code User->>Client: Authorization Code Client->>Auth Server: Code, Verifier & Client ID Auth Server->>Client: Access Token & Refresh Token Client->>Resource Server: Request with Access Token Resource Server->>Client: Protected Resource

PKCE (Proof Key for Code Exchange, pronounced "pixy") adds protection against code interception attacks by having the client prove it's the same application that initiated the flow.

It's like adding a unique password to your temporary pass request slip (the authorization code) so that only you can redeem it, even if someone else steals the slip.


// PKCE flow in a JavaScript SPA
// This would be part of your authentication service

class AuthService {
  constructor() {
    this.auth0Domain = 'your-domain.auth0.com';
    this.clientId = 'YOUR_CLIENT_ID';
    this.redirectUri = 'http://localhost:3000/callback';
    
    // Store in memory during the authorization flow
    this.codeVerifier = null;
    this.accessToken = null;
    this.refreshToken = null;
  }
  
  // Generate a random string for the code verifier
  generateCodeVerifier() {
    const array = new Uint8Array(32);
    window.crypto.getRandomValues(array);
    return this.base64URLEncode(array);
  }
  
  // Create a code challenge from the verifier
  async generateCodeChallenge(codeVerifier) {
    const encoder = new TextEncoder();
    const data = encoder.encode(codeVerifier);
    const digest = await window.crypto.subtle.digest('SHA-256', data);
    return this.base64URLEncode(new Uint8Array(digest));
  }
  
  base64URLEncode(array) {
    return btoa(String.fromCharCode.apply(null, array))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');
  }
  
  // Start the authentication flow
  async login() {
    // Generate and store PKCE code verifier
    this.codeVerifier = this.generateCodeVerifier();
    
    // Generate code challenge
    const codeChallenge = await this.generateCodeChallenge(this.codeVerifier);
    
    // Store code verifier in local storage or session storage
    localStorage.setItem('code_verifier', this.codeVerifier);
    
    // Build authorization URL
    const authUrl = new URL(`https://${this.auth0Domain}/authorize`);
    authUrl.searchParams.append('client_id', this.clientId);
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('redirect_uri', this.redirectUri);
    authUrl.searchParams.append('scope', 'openid profile email offline_access');
    authUrl.searchParams.append('code_challenge', codeChallenge);
    authUrl.searchParams.append('code_challenge_method', 'S256');
    
    // Redirect to authorization server
    window.location.href = authUrl.toString();
  }
  
  // Handle the callback from authorization server
  async handleCallback() {
    // Get authorization code from URL
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get('code');
    
    if (code) {
      // Retrieve code verifier from storage
      const codeVerifier = localStorage.getItem('code_verifier');
      
      // Exchange code for tokens
      try {
        const tokenResponse = await fetch(`https://${this.auth0Domain}/oauth/token`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: new URLSearchParams({
            grant_type: 'authorization_code',
            client_id: this.clientId,
            code_verifier: codeVerifier,
            code: code,
            redirect_uri: this.redirectUri
          })
        });
        
        if (!tokenResponse.ok) {
          throw new Error('Failed to exchange code for tokens');
        }
        
        const tokens = await tokenResponse.json();
        
        // Store tokens securely (consider memory-only storage for production)
        this.accessToken = tokens.access_token;
        this.refreshToken = tokens.refresh_token;
        
        // Clear code verifier from storage
        localStorage.removeItem('code_verifier');
        
        // Redirect to application
        window.location.href = '/';
      } catch (error) {
        console.error('Token exchange error:', error);
      }
    }
  }
  
  // Get user resource with access token
  async getResource() {
    if (!this.accessToken) {
      throw new Error('No access token available');
    }
    
    try {
      const response = await fetch('https://resource-server.com/api/data', {
        headers: {
          'Authorization': `Bearer ${this.accessToken}`
        }
      });
      
      if (!response.ok) {
        throw new Error('Failed to fetch resource');
      }
      
      return await response.json();
    } catch (error) {
      console.error('Resource fetch error:', error);
      throw error;
    }
  }
}
            

OAuth 2.0 Tokens

OAuth 2.0 uses different types of tokens for authorization and access control.

Access Tokens

Access tokens allow a client to access protected resources. They can be in any format, but are commonly JWTs in modern implementations.


// Example of using an access token
fetch('https://api.example.com/user/profile', {
  headers: {
    'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...'
  }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
            

Refresh Tokens

Refresh tokens are used to obtain new access tokens when the current one expires.


// Example of using a refresh token to get a new access token
async function refreshAccessToken(refreshToken) {
  try {
    const response = await fetch('https://authorization-server.com/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',
        client_secret: 'YOUR_CLIENT_SECRET'
      })
    });
    
    if (!response.ok) {
      throw new Error('Token refresh failed');
    }
    
    const data = await response.json();
    return {
      accessToken: data.access_token,
      refreshToken: data.refresh_token || refreshToken // Some servers issue new refresh tokens
    };
  } catch (error) {
    console.error('Error refreshing token:', error);
    throw error;
  }
}
            

ID Tokens

Specific to OpenID Connect (an extension of OAuth 2.0), ID tokens contain user identity information.


// Example ID Token payload
{
  "iss": "https://authorization-server.com",  // Issuer
  "sub": "user123",                         // Subject (user identifier)
  "aud": "client456",                       // Audience (client ID)
  "exp": 1619155200,                        // Expiration time
  "iat": 1619151600,                        // Issued at time
  "auth_time": 1619151590,                  // Time of authentication
  "nonce": "n-0S6_WzA2Mj",                  // Prevents replay attacks
  "name": "John Doe",                       // User's full name
  "given_name": "John",                     // First name
  "family_name": "Doe",                     // Last name
  "email": "john.doe@example.com",          // Email
  "email_verified": true                    // Email verification status
}
            

OAuth 2.0 Scopes

Scopes are a way to limit an application's access to a user's account. They specify what actions an application can perform on behalf of the user.

graph LR A[Application] --> B[Requests Scopes] B --> C{Consent Screen} C --> D[User Approves/Denies Scopes] D --> E[Token Issued with Approved Scopes] E --> F[Resource Server Enforces Scopes]

Think of scopes like permissions on your smartphone. When you install an app, it might request access to your camera, contacts, location, etc. You can grant or deny each permission individually, and the app can only access what you've explicitly allowed.

Common OAuth 2.0 Scopes

Each service provider defines its own set of scopes. For example, Google has scopes like https://www.googleapis.com/auth/drive.readonly for read-only access to Google Drive files.


// Example of requesting specific scopes during authorization
function redirectToAuth() {
  const authUrl = new URL('https://authorization-server.com/auth');
  authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.append('redirect_uri', 'https://your-app.com/callback');
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('scope', 'openid profile email calendar.read contacts.write');
  
  window.location.href = authUrl.toString();
}
            

Implementing Scope Checking

On the resource server, you need to verify that the access token has the necessary scopes for the requested operation.


// Express middleware to check scopes
function requireScopes(requiredScopes) {
  return (req, res, next) => {
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'Access token required' });
    }
    
    const token = authHeader.split(' ')[1];
    
    try {
      // Decode and verify JWT (implementation details vary)
      const decodedToken = jwt.verify(token, 'YOUR_SECRET_KEY');
      
      // Check if token has required scopes
      const tokenScopes = decodedToken.scope.split(' ');
      const hasRequiredScopes = requiredScopes.every(scope => 
        tokenScopes.includes(scope)
      );
      
      if (!hasRequiredScopes) {
        return res.status(403).json({ 
          error: 'Insufficient permissions',
          required_scopes: requiredScopes,
          provided_scopes: tokenScopes
        });
      }
      
      // Add token payload to request for later use
      req.user = decodedToken;
      next();
    } catch (error) {
      return res.status(401).json({ error: 'Invalid token' });
    }
  };
}

// Usage in routes
app.get('/api/profile', 
  requireScopes(['profile']), 
  (req, res) => {
    // Only accessible with 'profile' scope
    res.json({ name: req.user.name, email: req.user.email });
  }
);

app.post('/api/calendar/events', 
  requireScopes(['calendar.write']), 
  (req, res) => {
    // Only accessible with 'calendar.write' scope
    // Create calendar event...
    res.json({ status: 'Event created' });
  }
);
            

Introduction to OpenID Connect

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0, adding authentication capabilities to the authorization framework.

graph TD A[OpenID Connect] -->|Extends| B[OAuth 2.0] A -->|Adds| C[ID Token] A -->|Adds| D[UserInfo Endpoint] A -->|Adds| E[Standard Claims] A -->|Adds| F[Discovery] A -->|Adds| G[Dynamic Registration]

While OAuth 2.0 is designed for authorization ("what can you do?"), OpenID Connect is designed for authentication ("who are you?"). It enables a client to verify a user's identity and get basic profile information through a standardized REST-like API.

Continuing our analogy, if OAuth is like a valet key that grants limited access to your car, OpenID Connect is like adding a driver's license check before issuing that key - it verifies who the person is before granting them access.

Key Components of OpenID Connect

OpenID Connect Flows

OpenID Connect defines three authentication flows, built on top of the corresponding OAuth 2.0 flows:

Authorization Code Flow

The most secure flow, suitable for server-side applications.

sequenceDiagram participant User participant Client participant OIDC Provider User->>Client: Use Application Client->>User: Redirect to OIDC Provider User->>OIDC Provider: Authenticate OIDC Provider->>User: Redirect with Authorization Code User->>Client: Authorization Code Client->>OIDC Provider: Exchange Code for Tokens OIDC Provider->>Client: ID Token, Access Token, Refresh Token Note over Client: Validate ID Token Client->>OIDC Provider: Optional: Request UserInfo OIDC Provider->>Client: UserInfo Response

Implicit Flow

Less secure, designed for browser-based applications (not recommended for new implementations).

sequenceDiagram participant User participant Client participant OIDC Provider User->>Client: Use Application Client->>User: Redirect to OIDC Provider User->>OIDC Provider: Authenticate OIDC Provider->>User: Redirect with ID Token and Access Token in Fragment User->>Client: Tokens Note over Client: Validate ID Token Client->>OIDC Provider: Optional: Request UserInfo with Access Token OIDC Provider->>Client: UserInfo Response

Hybrid Flow

Combines aspects of both flows, returning some tokens from the authorization endpoint and others from the token endpoint.

sequenceDiagram participant User participant Client participant OIDC Provider User->>Client: Use Application Client->>User: Redirect to OIDC Provider User->>OIDC Provider: Authenticate OIDC Provider->>User: Redirect with Authorization Code and ID Token User->>Client: Code and ID Token Note over Client: Validate ID Token Client->>OIDC Provider: Exchange Code for Tokens OIDC Provider->>Client: Access Token and Refresh Token

Implementing OpenID Connect with Auth0

Let's look at a practical implementation of OpenID Connect using Auth0, a popular identity platform.

Setting Up Auth0

  1. Create an Auth0 account at auth0.com
  2. Create a new application (Regular Web Application for server-side apps or Single Page Application for client-side apps)
  3. Configure Allowed Callback URLs, Logout URLs, and Web Origins
  4. Note your Domain, Client ID, and Client Secret

Server-Side Implementation (Node.js/Express)


// npm install express express-session passport passport-auth0

const express = require('express');
const session = require('express-session');
const passport = require('passport');
const Auth0Strategy = require('passport-auth0');
const app = express();

// Configure session
app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: false
}));

// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

// Configure Auth0 strategy
passport.use(new Auth0Strategy({
    domain: 'your-auth0-domain.auth0.com',
    clientID: 'YOUR_CLIENT_ID',
    clientSecret: 'YOUR_CLIENT_SECRET',
    callbackURL: 'http://localhost:3000/callback',
    scope: 'openid profile email'
  },
  function(accessToken, refreshToken, extraParams, profile, done) {
    // extraParams.id_token has the ID token
    return done(null, {
      ...profile,
      id_token: extraParams.id_token,
      access_token: accessToken,
      refresh_token: refreshToken
    });
  }
));

// Serialize/deserialize user
passport.serializeUser((user, done) => {
  done(null, user);
});

passport.deserializeUser((user, done) => {
  done(null, user);
});

// Routes
app.get('/', (req, res) => {
  res.send(req.isAuthenticated() ? 
    `Logged in as ${req.user.displayName} <a href="/logout">Logout</a>` : 
    'Not logged in <a href="/login">Login</a>');
});

// Start login
app.get('/login', passport.authenticate('auth0', {
  scope: 'openid profile email'
}));

// Handle callback
app.get('/callback',
  passport.authenticate('auth0', { failureRedirect: '/' }),
  (req, res) => {
    res.redirect('/profile');
  }
);

// Protected profile route
app.get('/profile', ensureAuthenticated, (req, res) => {
  res.json({
    user: req.user._json,
    id_token: req.user.id_token
  });
});

// Logout
app.get('/logout', (req, res) => {
  req.logout();
  
  // Redirect to Auth0 logout to end the session
  const returnTo = encodeURIComponent('http://localhost:3000');
  res.redirect(`https://your-auth0-domain.auth0.com/v2/logout?client_id=${process.env.AUTH0_CLIENT_ID}&returnTo=${returnTo}`);
});

// Middleware to check if user is authenticated
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
}

app.listen(3000, () => {
  console.log('Server running on port 3000');
});
            

Client-Side Implementation (React)


// npm install @auth0/auth0-react

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Auth0Provider } from '@auth0/auth0-react';

ReactDOM.render(
  
    
  ,
  document.getElementById('root')
);

// src/App.js
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import Profile from './Profile';
import LoginButton from './LoginButton';
import LogoutButton from './LogoutButton';

function App() {
  const { isAuthenticated, isLoading } = useAuth0();

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>Auth0 OIDC Demo</h1>
      {isAuthenticated ? (
        <>
          <LogoutButton />
          <Profile />
        </>
      ) : (
        <LoginButton />
      )}
    </div>
  );
}

export default App;

// src/LoginButton.js
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const LoginButton = () => {
  const { loginWithRedirect } = useAuth0();

  return         <button onClick={() => loginWithRedirect()}>Log In</button>;
};

export default LoginButton;

// src/LogoutButton.js
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const LogoutButton = () => {
  const { logout } = useAuth0();

  return (
    <button onClick={() => logout({ returnTo: window.location.origin })}>
      Log Out
    </button>
  );
};

export default LogoutButton;

// src/Profile.js
import React, { useEffect, useState } from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const Profile = () => {
  const { user, isAuthenticated, getIdTokenClaims, getAccessTokenSilently } = useAuth0();
  const [idToken, setIdToken] = useState(null);
  const [apiData, setApiData] = useState(null);

  useEffect(() => {
    async function getTokens() {
      if (isAuthenticated) {
        // Get ID token claims
        const claims = await getIdTokenClaims();
        setIdToken(claims);
        
        // Call protected API with access token
        try {
          const accessToken = await getAccessTokenSilently();
          const response = await fetch('https://your-api.com/data', {
            headers: {
              Authorization: `Bearer ${accessToken}`
            }
          });
          
          const data = await response.json();
          setApiData(data);
        } catch (error) {
          console.error('API call failed:', error);
        }
      }
    }
    
    getTokens();
  }, [isAuthenticated, getIdTokenClaims, getAccessTokenSilently]);

  if (!isAuthenticated) {
    return <div>Please log in</div>;
  }

  return (
    

Profile

<img src={user.picture} alt={user.name} />

{user.name}

{user.email}

ID Token Claims

{JSON.stringify(idToken, null, 2)}

API Data

{JSON.stringify(apiData, null, 2)}
); }; export default Profile;

ID Token Validation

Properly validating the ID token is crucial for security in OpenID Connect implementations.

Required Validation Steps

  1. Verify signature: Check that the token was signed by the expected issuer
  2. Validate issuer: Ensure the iss claim matches your expected issuer
  3. Validate audience: Check that the aud claim includes your client ID
  4. Check expiration: Verify the exp claim to ensure the token hasn't expired
  5. Check issue time: Verify the iat claim to ensure the token wasn't issued in the future
  6. Verify nonce: If a nonce was sent with the auth request, validate it against the nonce claim

// ID Token validation in Node.js
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

// Setup the JWKS client
const client = jwksClient({
  jwksUri: `https://your-auth0-domain.auth0.com/.well-known/jwks.json`
});

// Function to get the signing key
function getSigningKey(kid) {
  return new Promise((resolve, reject) => {
    client.getSigningKey(kid, (err, key) => {
      if (err) {
        return reject(err);
      }
      const signingKey = key.getPublicKey();
      resolve(signingKey);
    });
  });
}

// Validate ID token
async function validateIdToken(idToken) {
  try {
    // Decode the token without verification to get the header
    const decoded = jwt.decode(idToken, { complete: true });
    
    if (!decoded || !decoded.header || !decoded.header.kid) {
      throw new Error('Invalid token');
    }
    
    // Get the signing key
    const signingKey = await getSigningKey(decoded.header.kid);
    
    // Verify the token
    const verified = jwt.verify(idToken, signingKey, {
      algorithms: ['RS256'],
      audience: 'YOUR_CLIENT_ID',
      issuer: 'https://your-auth0-domain.auth0.com/'
    });
    
    // Additional checks
    const now = Math.floor(Date.now() / 1000);
    
    if (verified.exp < now) {
      throw new Error('Token expired');
    }
    
    if (verified.iat > now) {
      throw new Error('Token issued in the future');
    }
    
    // If nonce was used, verify it
    if (sessionStorage.getItem('nonce') && verified.nonce !== sessionStorage.getItem('nonce')) {
      throw new Error('Invalid nonce');
    }
    
    return verified;
  } catch (error) {
    console.error('Token validation error:', error);
    throw error;
  }
}
            

Security Considerations

Both OAuth 2.0 and OpenID Connect have potential security vulnerabilities that need to be addressed.

Common Security Threats

State Parameter

The state parameter is used to prevent CSRF attacks. It's a random value sent in the authorization request and returned unchanged in the response.


// Generate and use state parameter
function startAuthentication() {
  // Generate random state
  const state = generateRandomString(32);
  
  // Store state in browser for later verification
  sessionStorage.setItem('oauth_state', state);
  
  // Include state in authorization request
  const authUrl = new URL('https://authorization-server.com/auth');
  authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.append('redirect_uri', 'https://your-app.com/callback');
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('scope', 'openid profile email');
  authUrl.searchParams.append('state', state);
  
  // Redirect to authorization server
  window.location.href = authUrl.toString();
}

// Verify state in callback
function handleCallback() {
  const urlParams = new URLSearchParams(window.location.search);
  const state = urlParams.get('state');
  const code = urlParams.get('code');
  
  // Verify state matches
  const savedState = sessionStorage.getItem('oauth_state');
  sessionStorage.removeItem('oauth_state'); // Clear stored state
  
  if (!state || state !== savedState) {
    // Potential CSRF attack
    console.error('State validation failed');
    return;
  }
  
  // Proceed with code exchange
  exchangeCodeForTokens(code);
}

function generateRandomString(length) {
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  const randomValues = new Uint8Array(length);
  window.crypto.getRandomValues(randomValues);
  
  randomValues.forEach(val => {
    result += charset[val % charset.length];
  });
  
  return result;
}
            

Best Practices

Building Your Own OAuth 2.0 / OIDC Provider

While most applications use existing OAuth providers, understanding how to build your own can provide valuable insights.

Key Components of an OAuth Provider

Building an OAuth Provider with Node.js

Let's explore a simplified implementation using the oauth2-server library:


// npm install express oauth2-server

const express = require('express');
const OAuth2Server = require('oauth2-server');
const Request = OAuth2Server.Request;
const Response = OAuth2Server.Response;

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// In-memory storage (use database in production)
const users = [
  { id: '1', username: 'user1', password: 'password1' }
];

const clients = [
  { id: 'client1', secret: 'secret1', redirectUris: ['http://localhost:3000/callback'] }
];

const authorizationCodes = [];
const tokens = [];

// OAuth server model
const model = {
  // Get client by ID
  getClient: function(clientId, clientSecret) {
    const client = clients.find(c => c.id === clientId);
    
    if (!client || (clientSecret && client.secret !== clientSecret)) {
      return false;
    }
    
    return {
      id: client.id,
      redirectUris: client.redirectUris,
      grants: ['authorization_code', 'refresh_token']
    };
  },
  
  // Save authorization code
  saveAuthorizationCode: function(code, client, user) {
    const authCode = {
      authorizationCode: code.authorizationCode,
      expiresAt: code.expiresAt,
      redirectUri: code.redirectUri,
      scope: code.scope,
      client: { id: client.id },
      user: { id: user.id }
    };
    
    authorizationCodes.push(authCode);
    return authCode;
  },
  
  // Get authorization code
  getAuthorizationCode: function(authorizationCode) {
    return authorizationCodes.find(code => code.authorizationCode === authorizationCode);
  },
  
  // Revoke authorization code
  revokeAuthorizationCode: function(code) {
    const index = authorizationCodes.findIndex(c => 
      c.authorizationCode === code.authorizationCode
    );
    
    if (index !== -1) {
      authorizationCodes.splice(index, 1);
      return true;
    }
    
    return false;
  },
  
  // Save token
  saveToken: function(token, client, user) {
    const newToken = {
      accessToken: token.accessToken,
      accessTokenExpiresAt: token.accessTokenExpiresAt,
      refreshToken: token.refreshToken,
      refreshTokenExpiresAt: token.refreshTokenExpiresAt,
      scope: token.scope,
      client: { id: client.id },
      user: { id: user.id }
    };
    
    tokens.push(newToken);
    return newToken;
  },
  
  // Get access token
  getAccessToken: function(accessToken) {
    return tokens.find(token => token.accessToken === accessToken);
  },
  
  // Get refresh token
  getRefreshToken: function(refreshToken) {
    return tokens.find(token => token.refreshToken === refreshToken);
  },
  
  // Revoke refresh token
  revokeToken: function(token) {
    const index = tokens.findIndex(t => t.refreshToken === token.refreshToken);
    
    if (index !== -1) {
      tokens.splice(index, 1);
      return true;
    }
    
    return false;
  },
  
  // Verify scope
  verifyScope: function(token, scope) {
    if (!token.scope) {
      return false;
    }
    
    const requestedScopes = scope.split(' ');
    const authorizedScopes = token.scope.split(' ');
    
    return requestedScopes.every(s => authorizedScopes.includes(s));
  },
  
  // Get user
  getUser: function(username, password) {
    const user = users.find(u => u.username === username);
    
    if (!user || user.password !== password) {
      return false;
    }
    
    return { id: user.id };
  },
  
  // Get user from client
  getUserFromClient: function(client) {
    // For client credentials grant (not implemented in this example)
    return false;
  }
};

// Create OAuth server
const oauth = new OAuth2Server({
  model: model,
  accessTokenLifetime: 3600, // 1 hour
  refreshTokenLifetime: 1209600, // 14 days
  allowBearerTokensInQueryString: true
});

// Middleware to authenticate requests
function authenticateHandler(req, res, next) {
  const request = new Request(req);
  const response = new Response(res);
  
  return oauth.authenticate(request, response)
    .then(token => {
      req.user = token.user;
      req.client = token.client;
      req.scope = token.scope;
      next();
    })
    .catch(err => {
      res.status(err.code || 500).json(err);
    });
}

// Authorization endpoint
app.get('/authorize', async (req, res) => {
  const { client_id, redirect_uri, response_type, scope, state } = req.query;
  
  // In a real implementation, you would:
  // 1. Authenticate the user (login form)
  // 2. Show consent screen
  // 3. Process user's decision
  
  // For simplicity, we'll skip the UI and assume user consent
  
  // Validate client and redirect URI
  const client = clients.find(c => c.id === client_id);
  
  if (!client || !client.redirectUris.includes(redirect_uri)) {
    return res.status(400).json({ error: 'invalid_client' });
  }
  
  if (response_type !== 'code') {
    return res.status(400).json({ error: 'unsupported_response_type' });
  }
  
  // Generate authorization code
  const authCode = {
    authorizationCode: Math.random().toString(36).substring(2, 15),
    expiresAt: new Date(Date.now() + 600000), // 10 minutes
    redirectUri: redirect_uri,
    scope: scope || '',
    client: { id: client_id },
    user: { id: '1' } // Assume user 1 for this example
  };
  
  authorizationCodes.push(authCode);
  
  // Redirect with code
  const redirectUrl = new URL(redirect_uri);
  redirectUrl.searchParams.append('code', authCode.authorizationCode);
  
  if (state) {
    redirectUrl.searchParams.append('state', state);
  }
  
  res.redirect(redirectUrl.toString());
});

// Token endpoint
app.post('/token', (req, res) => {
  const request = new Request(req);
  const response = new Response(res);
  
  return oauth.token(request, response)
    .then(token => {
      res.json({
        access_token: token.accessToken,
        token_type: 'Bearer',
        expires_in: Math.floor((token.accessTokenExpiresAt - new Date()) / 1000),
        refresh_token: token.refreshToken,
        scope: token.scope
      });
    })
    .catch(err => {
      res.status(err.code || 500).json(err);
    });
});

// Protected resource endpoint
app.get('/resource', authenticateHandler, (req, res) => {
  res.json({
    message: 'Protected resource',
    user_id: req.user.id
  });
});

// UserInfo endpoint (for OpenID Connect)
app.get('/userinfo', authenticateHandler, (req, res) => {
  const user = users.find(u => u.id === req.user.id);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  // Only return fields authorized by scope
  const scope = req.scope ? req.scope.split(' ') : [];
  const userinfo = {
    sub: user.id
  };
  
  if (scope.includes('profile')) {
    userinfo.name = 'User One';
    userinfo.preferred_username = user.username;
  }
  
  if (scope.includes('email')) {
    userinfo.email = 'user1@example.com';
    userinfo.email_verified = true;
  }
  
  res.json(userinfo);
});

// OpenID Connect discovery document
app.get('/.well-known/openid-configuration', (req, res) => {
  const baseUrl = 'http://localhost:3000';
  
  res.json({
    issuer: baseUrl,
    authorization_endpoint: `${baseUrl}/authorize`,
    token_endpoint: `${baseUrl}/token`,
    userinfo_endpoint: `${baseUrl}/userinfo`,
    jwks_uri: `${baseUrl}/.well-known/jwks.json`,
    response_types_supported: ['code'],
    subject_types_supported: ['public'],
    id_token_signing_alg_values_supported: ['RS256'],
    scopes_supported: ['openid', 'profile', 'email'],
    token_endpoint_auth_methods_supported: ['client_secret_basic'],
    claims_supported: ['sub', 'name', 'preferred_username', 'email', 'email_verified']
  });
});

app.listen(3000, () => {
  console.log('OAuth 2.0 / OIDC server running on port 3000');
});
            

This is a simplified example. A production-ready OAuth provider would include:

Real-World Use Cases

Single Sign-On (SSO)

Using a single identity provider to authenticate across multiple applications.

graph TD A[User] --> B[Identity Provider] B --> C[App 1] B --> D[App 2] B --> E[App 3]

Example: A corporation using Azure AD as the identity provider for its internal applications.

Third-Party API Access

Allowing third-party applications to access a platform's API on behalf of users.

graph TD A[User] --> B[Third-Party App] B --> C[Platform API] C --> D[User's Data]

Example: Using OAuth to allow a third-party application to post tweets on a user's behalf via the Twitter API.

Microservices Authentication

Using OAuth/OIDC for authentication and authorization in a microservices architecture.

graph TD A[User] --> B[Client App] B --> C[API Gateway] C --> D[Auth Service] C --> E[Service 1] C --> F[Service 2] C --> G[Service 3] D --> H[Identity Provider]

Example: An e-commerce platform where the API gateway validates tokens before forwarding requests to microservices.

Mobile App Authentication

Using OAuth/OIDC to authenticate users in mobile applications.

graph TD A[User] --> B[Mobile App] B --> C[Backend API] B --> D[Identity Provider] D --> B

Example: A mobile banking app that uses OAuth 2.0 with PKCE to authenticate users.

Practical Activities

Activity 1: Implement Social Login

Add Google or Facebook login to an existing application:

  1. Register your application with the provider (Google or Facebook)
  2. Implement the OAuth flow using Passport.js or a similar library
  3. Store user profile information and link it to your application's user accounts
  4. Add a "Login with Google/Facebook" button to your UI
  5. Implement logout functionality

Activity 2: Create an OAuth Client

Build a simple client application that uses OAuth for authentication:

  1. Choose an OAuth provider (Auth0, Okta, or build your own)
  2. Implement the authorization code flow with PKCE
  3. Store tokens securely and handle token refresh
  4. Create a protected resource endpoint that requires a valid token
  5. Implement proper error handling and security measures

Activity 3: Build a Simple OpenID Connect Provider

Extend the OAuth provider example to support OpenID Connect:

  1. Add support for the openid scope
  2. Implement ID token generation with JWT
  3. Create a UserInfo endpoint
  4. Add the discovery document endpoint
  5. Implement a JWKS endpoint for key distribution

Activity 4: Implement Single Sign-On

Create a simple SSO system with multiple applications:

  1. Set up an identity provider (Auth0, Okta, or your own)
  2. Create two or more client applications
  3. Configure all applications to use the same identity provider
  4. Implement login and session management
  5. Test single sign-on functionality across applications

Additional Resources

Specifications

Libraries and Tools

Providers and Services

Tutorials and Guides