mdashikjs/blog
All posts
OAuth 2.0 and OpenID Connect: A Practical Implementation Guide
Security

OAuth 2.0 and OpenID Connect: A Practical Implementation Guide

Security6 min

OAuth 2.0 and OpenID Connect: A Practical Implementation Guide

OAuth 2.0 is an authorization framework, not an authentication protocol. OpenID Connect adds the authentication layer on top. Getting this distinction wrong is the root of most auth bugs I have debugged.

OAuthOpenID ConnectAuthenticationSecurity
Share:

The core distinction

  • OAuth 2.0: "This app is allowed to read your Google Drive files." Authorization.
  • OpenID Connect: "This person is john@example.com and they proved it via Google." Authentication.

If you are building "Sign in with Google," you need OpenID Connect, not plain OAuth 2.0.

The Authorization Code flow

This is the flow for server-rendered or server-backed apps. It is the most secure because the access token never touches the browser:

1. User clicks "Sign in with Google"
2. Browser redirects to Google's authorization endpoint
3. User authenticates with Google
4. Google redirects back to your app with an authorization code
5. Your server exchanges the code for tokens (server-to-server)
6. Your server gets an access token + ID token

Server-side implementation

import { Router } from 'express';
import { generators, Issuer } from 'openid-client';

const googleIssuer = await Issuer.discover('https://accounts.google.com');
const client = new googleIssuer.Client({
  client_id: process.env.GOOGLE_CLIENT_ID!,
  client_secret: process.env.GOOGLE_CLIENT_SECRET!,
  redirect_uris: ['https://myapp.com/auth/callback'],
  response_types: ['code'],
});

router.get('/auth/login', (req, res) => {
  const nonce = generators.nonce();
  const state = generators.state();
  req.session.nonce = nonce;
  req.session.state = state;

  const url = client.authorizationUrl({
    scope: 'openid email profile',
    nonce,
    state,
  });
  res.redirect(url);
});

router.get('/auth/callback', async (req, res) => {
  const params = client.callbackParams(req);
  const tokenSet = await client.callback(
    'https://myapp.com/auth/callback',
    params,
    { nonce: req.session.nonce, state: req.session.state },
  );

  const userInfo = tokenSet.claims();
  // userInfo.sub, userInfo.email, userInfo.name

  const user = await findOrCreateUser(userInfo);
  req.session.userId = user.id;
  res.redirect('/dashboard');
});

PKCE: required for public clients

Single-page apps and mobile apps cannot keep a client secret. PKCE (Proof Key for Code Exchange) adds a challenge-response mechanism:

const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);

// Include in authorization request
const url = client.authorizationUrl({
  scope: 'openid email profile',
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
});

// Include verifier in token exchange
const tokenSet = await client.callback(redirectUri, params, { code_verifier: codeVerifier });

The authorization server verifies that the same client that started the flow is the one exchanging the code.

Token validation

Never trust an ID token without verifying:

  1. Signature: the token is signed by the issuer's private key
  2. Issuer: the iss claim matches the expected issuer
  3. Audience: the aud claim matches your client ID
  4. Expiration: the exp claim is in the future
  5. Nonce: matches what you stored in the session

The openid-client library handles all of this automatically in the callback() method.

Common mistakes I have seen

  1. Using the access token as proof of identity. Access tokens are for APIs, not for identifying users. Use the ID token.
  2. Storing tokens in localStorage. XSS can steal them. Use httpOnly cookies or server-side sessions.
  3. Skipping the state parameter. Without it, CSRF attacks can log users into the attacker's account.
  4. Not validating the ID token. Just because it came from the token endpoint does not mean it is valid. Always verify.

When to use which flow

  • Authorization Code + PKCE: SPAs, mobile apps, any public client
  • Authorization Code: server-rendered apps with a client secret
  • Client Credentials: machine-to-machine communication, no user involved
MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.