How OAuth 2.0 and OpenID Connect Work - Authentication and Authorization Design Principles

18 min read | 2025.12.02

In modern web and mobile applications, social logins like “Login with Google” or “Login with GitHub” have become commonplace. Behind these are two standard specifications: OAuth 2.0 and OpenID Connect (OIDC). This article explains these mechanisms from basics to advanced applications.

Differences Between OAuth 2.0 and OIDC

First, let’s clarify the often-confused differences between OAuth 2.0 and OIDC.

ItemOAuth 2.0OpenID Connect
PurposeAuthorizationAuthentication
Question answered”Will you allow this app access to resources?""Who are you?”
What you getAccess tokenID token + Access token
Year established2012 (RFC 6749)2014

Difference between Authentication and Authorization:

  • Authentication: Process of confirming “who the user is”
  • Authorization: Process of determining “what the user can access”

OAuth 2.0 Actors

OAuth 2.0 has four main roles.

flowchart TB
    RO["Resource Owner (User)<br/>Owner of the data"]
    AS["Authorization Server<br/>Issues tokens (Google, GitHub, etc.)"]
    Client["Client<br/>Application that wants to access resources"]
    RS["Resource Server<br/>Hosts protected resources (APIs, etc.)"]

    RO -->|"Grants authorization"| AS
    AS -->|"Issues token"| Client
    Client -->|"API request"| RS

OAuth 2.0 Grant Types

OAuth 2.0 defines multiple “grant types” (flows) for different use cases.

1. Authorization Code Grant

The most secure and recommended flow. Used in server-side applications.

sequenceDiagram
    participant U as User
    participant C as Client
    participant A as Auth Server
    participant R as Resource Server

    U->>C: 1. Start login
    C->>A: 2. Auth request
    A->>U: 3. Login screen / consent
    U->>A: 4. Authenticate/consent
    A->>C: 5. Auth code
    C->>A: 6. Token exchange
    A->>C: 7. Access token
    C->>R: 8. API call
    R->>C: 9. Resource
// Step 2: Generate authorization request URL
const authorizationUrl = new URL('https://auth.example.com/authorize');
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('client_id', CLIENT_ID);
authorizationUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authorizationUrl.searchParams.set('scope', 'openid profile email');
authorizationUrl.searchParams.set('state', generateRandomState());

// Step 6: Token exchange
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
  }),
});

2. Authorization Code Grant with PKCE

An extension for environments like SPAs and mobile apps where client secrets cannot be stored securely.

// PKCE: Proof Key for Code Exchange
import crypto from 'crypto';

// Step 1: Generate Code Verifier (43-128 random characters)
const codeVerifier = crypto.randomBytes(32)
  .toString('base64url');

// Step 2: Generate Code Challenge
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');

// Step 3: Add code_challenge to authorization request
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateState());

// Step 4: Send code_verifier during token exchange
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: codeVerifier,  // Send verifier instead of secret
  }),
});

3. Client Credentials Grant

Used for server-to-server communication (M2M: Machine to Machine). No user involvement.

// Backend service authentication
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'api:read api:write',
  }),
});

const { access_token } = await tokenResponse.json();

4. Refresh Token Grant

Flow for renewing access tokens.

// Renewing access token with refresh token
const refreshResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'refresh_token',
    refresh_token: storedRefreshToken,
    client_id: CLIENT_ID,
  }),
});

const { access_token, refresh_token } = await refreshResponse.json();
// Update storage if new refresh token is returned

OpenID Connect (OIDC)

OIDC is an authentication layer built on top of OAuth 2.0.

ID Token Structure

In OIDC, an ID token (JWT format) is issued in addition to the access token.

SectionContent
Header{"alg": "RS256", "typ": "JWT", "kid": "key-id-123"}
Payloadiss (Issuer), sub (Subject), aud (Audience), exp (Expiration), iat (Issued at), nonce (Replay prevention), email, name
SignatureSigned with authorization server’s private key

ID Token Validation

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

// JWKS (JSON Web Key Set) client configuration
const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  rateLimit: true,
});

// Function to get public key
const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
  client.getSigningKey(header.kid, (err, key) => {
    callback(err, key?.getPublicKey());
  });
};

// Verify ID token
const verifyIdToken = (idToken: string): Promise<jwt.JwtPayload> => {
  return new Promise((resolve, reject) => {
    jwt.verify(
      idToken,
      getKey,
      {
        algorithms: ['RS256'],
        issuer: 'https://auth.example.com',
        audience: CLIENT_ID,
      },
      (err, decoded) => {
        if (err) reject(err);
        else resolve(decoded as jwt.JwtPayload);
      }
    );
  });
};

Scope Design

OIDC Standard Scopes

ScopeReturned Claims
openidsub (required)
profilename, family_name, given_name, picture, etc.
emailemail, email_verified
addressaddress
phonephone_number, phone_number_verified

Custom Scope Example

// Custom scopes for API access
const scopes = [
  'openid',           // OIDC required
  'profile',          // Basic profile
  'email',            // Email address
  'api:read',         // API read permission
  'api:write',        // API write permission
  'admin:users',      // User management permission
];

Security Best Practices

1. CSRF Protection with State Parameter

// Generate unique state for each session
const generateState = (): string => {
  const state = crypto.randomBytes(32).toString('hex');
  // Save to session
  session.oauthState = state;
  return state;
};

// Validate state on callback
const validateState = (receivedState: string): boolean => {
  const isValid = receivedState === session.oauthState;
  delete session.oauthState;  // Delete after use
  return isValid;
};

2. Replay Attack Prevention with Nonce

// Include nonce in ID token request
const nonce = crypto.randomBytes(16).toString('hex');
session.oidcNonce = nonce;

authUrl.searchParams.set('nonce', nonce);

// Verify nonce when validating ID token
const decoded = await verifyIdToken(idToken);
if (decoded.nonce !== session.oidcNonce) {
  throw new Error('Invalid nonce');
}

3. Secure Token Storage

// Backend: Session management with HTTPOnly Cookie
res.cookie('session_id', sessionId, {
  httpOnly: true,      // Not accessible from JavaScript
  secure: true,        // HTTPS required
  sameSite: 'lax',     // CSRF protection
  maxAge: 3600000,     // 1 hour
});

// Frontend (SPA): Keep in memory
// Avoid LocalStorage as it's vulnerable to XSS
class TokenStore {
  private accessToken: string | null = null;

  setToken(token: string) {
    this.accessToken = token;
  }

  getToken() {
    return this.accessToken;
  }
}

4. Token Expiration Design

Token TypeRecommended Expiration
Access token15 min - 1 hour
Refresh token7 - 30 days (absolute)
ID token5 min - 1 hour
Authorization codeWithin 10 minutes

Summary

OAuth 2.0 and OIDC are foundational technologies for modern web authentication and authorization.

OAuth 2.0 Key Points

  • Purpose: Authorization (delegation of resource access rights)
  • Recommended flow: Authorization Code Grant + PKCE
  • Tokens: Access token, refresh token

OIDC Key Points

  • Purpose: Authentication (user identity verification)
  • Added element: ID token (JWT format)
  • Standard scopes: openid, profile, email, etc.

Security Requirements

  1. Use PKCE (especially for SPA/mobile)
  2. CSRF protection with state parameter
  3. Replay attack prevention with nonce
  4. Token storage with HTTPOnly Cookie
  5. Short access token expiration

By correctly understanding and implementing these concepts, you can build secure and user-friendly authentication systems.

← Back to list