OAuth 2.0 and OIDC Implementation Guide: From Theory to Production

Implement OAuth 2.0 and OpenID Connect correctly. Authorization code flow with PKCE, token management, refresh token rotation, and integration with...

Y
Yash Pritwani
16 min read

OAuth 2.0 and OIDC: The Foundation of Modern Auth

OAuth 2.0 is the authorization framework. OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. Together, they power "Login with Google/GitHub/Microsoft" and single sign-on (SSO) for enterprise applications.

FirewallWAFSSO / MFATLS/SSLRBACAudit Logs

Defense in depth: multiple security layers protect your infrastructure from threats.

The Flows

Authorization Code Flow with PKCE (Recommended)

This is the recommended flow for all modern applications (web, mobile, SPA):

1. User clicks "Login"
2. App generates code_verifier (random string) and code_challenge (SHA256 hash)
3. App redirects to Authorization Server:
   GET /authorize?
     response_type=code&
     client_id=my-app&
     redirect_uri=https://app.example.com/callback&
     scope=openid profile email&
     state=random-csrf-token&
     code_challenge=SHA256_HASH&
     code_challenge_method=S256

4. User authenticates with the Authorization Server
5. Authorization Server redirects back:
   GET /callback?code=AUTH_CODE&state=random-csrf-token

6. App exchanges code for tokens (server-side):
   POST /token
     grant_type=authorization_code&
     code=AUTH_CODE&
     redirect_uri=https://app.example.com/callback&
     client_id=my-app&
     client_secret=SECRET&
     code_verifier=ORIGINAL_RANDOM_STRING

7. Authorization Server returns:
   {
     "access_token": "eyJhbG...",
     "id_token": "eyJhbG...",
     "refresh_token": "dGhpcy...",
     "token_type": "Bearer",
     "expires_in": 3600
   }

Implementation in Node.js

import { randomBytes, createHash } from 'crypto';
import express from 'express';

const app = express();

const OIDC_CONFIG = {
  issuer: 'https://auth.techsaas.cloud',
  authorizationEndpoint: 'https://auth.techsaas.cloud/api/oidc/authorization',
  tokenEndpoint: 'https://auth.techsaas.cloud/api/oidc/token',
  userinfoEndpoint: 'https://auth.techsaas.cloud/api/oidc/userinfo',
  clientId: 'my-app',
  clientSecret: process.env.OIDC_CLIENT_SECRET,
  redirectUri: 'https://app.example.com/auth/callback',
  scopes: ['openid', 'profile', 'email'],
};

// Generate PKCE challenge
function generatePKCE() {
  const verifier = randomBytes(32).toString('base64url');
  const challenge = createHash('sha256').update(verifier).digest('base64url');
  return { verifier, challenge };
}

// Step 1: Redirect to login
app.get('/auth/login', (req, res) => {
  const state = randomBytes(16).toString('hex');
  const { verifier, challenge } = generatePKCE();

  // Store in session (server-side)
  req.session.oauthState = state;
  req.session.codeVerifier = verifier;

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: OIDC_CONFIG.clientId,
    redirect_uri: OIDC_CONFIG.redirectUri,
    scope: OIDC_CONFIG.scopes.join(' '),
    state: state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  });

  res.redirect(OIDC_CONFIG.authorizationEndpoint + '?' + params.toString());
});

// Step 2: Handle callback
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state (CSRF protection)
  if (state !== req.session.oauthState) {
    return res.status(403).json({ error: 'Invalid state' });
  }

  // Exchange code for tokens
  const tokenResponse = await fetch(OIDC_CONFIG.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code as string,
      redirect_uri: OIDC_CONFIG.redirectUri,
      client_id: OIDC_CONFIG.clientId,
      client_secret: OIDC_CONFIG.clientSecret,
      code_verifier: req.session.codeVerifier,
    }),
  });

  const tokens = await tokenResponse.json();

  // Validate ID token
  const claims = decodeJWT(tokens.id_token);
  if (claims.iss !== OIDC_CONFIG.issuer) throw new Error('Invalid issuer');
  if (claims.aud !== OIDC_CONFIG.clientId) throw new Error('Invalid audience');
  if (claims.exp < Date.now() / 1000) throw new Error('Token expired');

  // Store tokens in session
  req.session.accessToken = tokens.access_token;
  req.session.refreshToken = tokens.refresh_token;
  req.session.user = {
    sub: claims.sub,
    name: claims.name,
    email: claims.email,
  };

  // Clean up
  delete req.session.oauthState;
  delete req.session.codeVerifier;

  res.redirect('/dashboard');
});

// Step 3: Refresh tokens
async function refreshAccessToken(refreshToken) {
  const response = await fetch(OIDC_CONFIG.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: OIDC_CONFIG.clientId,
      client_secret: OIDC_CONFIG.clientSecret,
    }),
  });

  return response.json();
}

Get more insights on Security

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

Setting Up an OIDC Provider

Authelia (Self-Hosted)

At TechSaaS, we use Authelia as our OIDC provider for all services:

# authelia/config.yml (OIDC section)
identity_providers:
  oidc:
    hmac_secret: 'a-long-random-secret'
    jwks:
      - key_id: 'main'
        algorithm: 'RS256'
        use: 'sig'
        key: |
          -----BEGIN RSA PRIVATE KEY-----
          (generated with: openssl genrsa -out oidc-jwks.pem 4096)
          -----END RSA PRIVATE KEY-----
    clients:
      - client_id: 'gitea'
        client_name: 'Gitea'
        client_secret: 'hashed_secret_here'
        public: false
        authorization_policy: 'two_factor'
        redirect_uris:
          - 'https://git.techsaas.cloud/user/oauth2/Authelia/callback'
        scopes:
          - 'openid'
          - 'profile'
          - 'email'
          - 'groups'
        token_endpoint_auth_method: 'client_secret_basic'

      - client_id: 'bookstack'
        client_name: 'BookStack'
        client_secret: 'another_hashed_secret'
        public: false
        authorization_policy: 'one_factor'
        redirect_uris:
          - 'https://docs.techsaas.cloud/oidc/callback'
        scopes:
          - 'openid'
          - 'profile'
          - 'email'

      - client_id: 'my-custom-app'
        client_name: 'My App'
        client_secret: 'app_hashed_secret'
        public: false
        authorization_policy: 'one_factor'
        redirect_uris:
          - 'https://app.techsaas.cloud/auth/callback'
          - 'http://localhost:3000/auth/callback'
        scopes:
          - 'openid'
          - 'profile'
          - 'email'
          - 'groups'
        grant_types:
          - 'authorization_code'
          - 'refresh_token'
        response_types:
          - 'code'
UserIdentityVerifyPolicyEngineAccessProxyAppMFA + DeviceLeast PrivilegeEncrypted TunnelNever Trust, Always Verify

Zero Trust architecture: every request is verified through identity, policy, and access proxy layers.

Token Management Best Practices

Access Token

  • Short-lived (15 minutes to 1 hour)
  • Used for API authentication
  • Sent in Authorization header
  • Do not store in localStorage (XSS vulnerable)

Refresh Token

  • Long-lived (days to weeks)
  • Used only to get new access tokens
  • Store in HTTP-only secure cookie (web) or secure storage (mobile)
  • Implement rotation (new refresh token on each use)

ID Token

  • Contains user identity claims
  • Validate on the client side (check iss, aud, exp)
  • Do not send to APIs (use access token instead)

Token Storage Patterns

Storage Web Mobile Security
HTTP-only cookie Best for web N/A Safe from XSS
localStorage Not recommended N/A XSS vulnerable
sessionStorage Acceptable N/A Tab-scoped, XSS risk
Secure Keychain N/A Best OS-level encryption
In-memory Good (short sessions) Good Lost on refresh

Recommended web pattern:

// Backend for Frontend (BFF) pattern
// Access token stored in-memory on the server
// Session cookie (HTTP-only, secure, SameSite) links to server session
// Refresh token stored server-side, never sent to browser

app.get('/api/data', async (req, res) => {
  // Session cookie identifies the user
  const session = getSession(req.cookies.session_id);
  if (!session) return res.status(401).json({ error: 'Not authenticated' });

  // Access token stored server-side
  let accessToken = session.accessToken;

  // Refresh if expired
  if (isExpired(accessToken)) {
    const tokens = await refreshAccessToken(session.refreshToken);
    session.accessToken = tokens.access_token;
    session.refreshToken = tokens.refresh_token; // Rotation
    accessToken = tokens.access_token;
  }

  // Proxy API call with access token
  const data = await fetch('https://api.example.com/data', {
    headers: { Authorization: 'Bearer ' + accessToken },
  });

  res.json(await data.json());
});

Common OIDC Security Mistakes

Free Resource

Infrastructure Security Audit Template

The exact audit template we use with clients: 60+ checks across network, identity, secrets management, and compliance.

Get the Template
  1. Not validating the state parameter: Enables CSRF attacks
  2. Not validating ID token claims: Anyone could forge tokens
  3. Storing tokens in localStorage: XSS attacks can steal them
  4. Not implementing PKCE: Authorization code interception attacks
  5. Long-lived access tokens: Should be 15-60 minutes, not days
  6. Not rotating refresh tokens: Stolen refresh tokens have unlimited life
  7. Accepting tokens from wrong issuer: Validate the iss claim
  8. Not checking token expiration: Always verify exp claim
Hello WorldPlaintextEncryptAES-256🔑x8f2...k9zCiphertextDecryptAES-256🔑Symmetric Encryption: same key encrypts and decrypts

Encryption transforms readable plaintext into unreadable ciphertext, reversible only with the correct key.

Forward Auth vs OIDC: When to Use Each

Approach Forward Auth (Traefik + Authelia) OIDC Integration
Implementation Zero code (middleware) Application code
User experience Redirect to Authelia login "Login with SSO" button
Fine-grained access URL/path based Scopes and claims
API support Cookie-based (browsers only) Bearer tokens (any client)
Best for Protecting internal tools Custom applications

At TechSaaS, we use both:

  • Forward auth for internal tools (Dozzle, Grafana, n8n): Zero code, just add Traefik middleware labels
  • OIDC for applications that need user identity (Gitea, BookStack): Full SSO with user context

This dual approach gives us SSO across all 33 services without modifying most of them. Only applications that need to know who the user is (for authorization, personalization, or audit) use OIDC directly.

#oauth2#oidc#authentication#authelia#security

Related Service

Security & Compliance

Zero-trust architecture, compliance automation, and incident response planning.

Need help with security?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.