🔥 0
0
Lesson 4 of 6 20 min +200 XP

OpenID Connect

OAuth 2.0 was designed for authorization - letting apps access resources. But developers kept using it for authentication - logging users in. This worked, but inconsistently.

OpenID Connect (OIDC) standardizes authentication on top of OAuth 2.0.

OAuth 2.0

Authorization

"Can this app access my photos?"

OpenID Connect

Authentication

"Who is this user?"

What OIDC Adds to OAuth

🎫
ID Token

JWT with user identity

👤
UserInfo Endpoint

Standard profile API

📋
Standard Claims

name, email, picture, etc.

🔍
Discovery

Auto-configuration

The ID Token

The ID token is a JWT that contains identity claims about the user.

{
  "iss": "https://accounts.google.com",
  "sub": "110248495921238986420",
  "aud": "your-client-id.apps.googleusercontent.com",
  "exp": 1699999999,
  "iat": 1699996399,
  "nonce": "abc123",
  "email": "john@example.com",
  "email_verified": true,
  "name": "John Doe",
  "picture": "https://lh3.googleusercontent.com/a/photo.jpg",
  "given_name": "John",
  "family_name": "Doe"
}

Standard OIDC Claims

Claim Description Scope Required
sub Unique user identifier openid
name Full name profile
email Email address email
email_verified Is email verified? email
picture Profile photo URL profile

OIDC Flow in Action

The OIDC flow is identical to OAuth Authorization Code, but with identity added.

// Step 1: Authorization request with OIDC scopes
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile'); // OIDC scopes!
authUrl.searchParams.set('state', generateState());
authUrl.searchParams.set('nonce', generateNonce()); // OIDC: prevents replay attacks

// Step 2: Token exchange returns ID token too
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    code: authorizationCode,
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    redirect_uri: 'https://yourapp.com/callback',
    grant_type: 'authorization_code'
  })
});

const tokens = await tokenResponse.json();
// {
//   access_token: "ya29.a0...",
//   id_token: "eyJhbGciOiJSUzI1NiIs...",  <-- This is new!
//   expires_in: 3599,
//   token_type: "Bearer",
//   scope: "openid email profile"
// }

// Step 3: Validate and decode ID token
const idToken = tokens.id_token;
const payload = decodeAndVerifyIdToken(idToken);

console.log(payload.email);   // "john@example.com"
console.log(payload.name);    // "John Doe"
console.log(payload.picture); // Profile photo URL

ID Token Validation

Critical: Always Validate ID Tokens

Never trust an ID token without validating it. Attackers can create fake JWTs. Validation ensures the token came from the expected issuer and wasn't tampered with.

// Using a library like jose for validation
import { jwtVerify, createRemoteJWKSet } from 'jose';

async function validateIdToken(idToken) {
  // Fetch Google's public keys
  const JWKS = createRemoteJWKSet(
    new URL('https://www.googleapis.com/oauth2/v3/certs')
  );

  try {
    const { payload } = await jwtVerify(idToken, JWKS, {
      issuer: 'https://accounts.google.com',
      audience: CLIENT_ID
    });

    // Additional checks
    if (payload.nonce !== expectedNonce) {
      throw new Error('Invalid nonce - possible replay attack');
    }

    return payload; // User info!
  } catch (error) {
    throw new Error('Invalid ID token: ' + error.message);
  }
}

Validation Checklist

  • Verify signature - Using issuer's public keys (JWKS)
  • Check issuer (iss) - Must match expected auth server
  • Check audience (aud) - Must include your client ID
  • Check expiration (exp) - Token must not be expired
  • Verify nonce - Matches what you sent (prevents replay)

The UserInfo Endpoint

Want more user details? Call the UserInfo endpoint with the access token.

// Get additional user info
const userInfoResponse = await fetch(
  'https://openidconnect.googleapis.com/v1/userinfo',
  {
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  }
);

const userInfo = await userInfoResponse.json();
// {
//   "sub": "110248495921238986420",
//   "name": "John Doe",
//   "given_name": "John",
//   "family_name": "Doe",
//   "picture": "https://lh3.googleusercontent.com/...",
//   "email": "john@example.com",
//   "email_verified": true,
//   "locale": "en"
// }

OIDC Discovery

OIDC providers publish their configuration at a well-known URL:

https://accounts.google.com/.well-known/openid-configuration
{
  "issuer": "https://accounts.google.com",
  "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
  "token_endpoint": "https://oauth2.googleapis.com/token",
  "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
  "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
  "scopes_supported": ["openid", "email", "profile"],
  "response_types_supported": ["code", "token", "id_token"],
  "claims_supported": ["sub", "name", "email", "picture", ...]
}
Auto-Configuration

With discovery, your app can automatically configure itself for any OIDC provider. Just fetch the discovery document and use the endpoints it specifies.

Real-World: "Sign in with Google"

Here's a complete implementation:

// 1. Configuration from discovery
const GOOGLE_CONFIG = {
  authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenEndpoint: 'https://oauth2.googleapis.com/token',
  jwksUri: 'https://www.googleapis.com/oauth2/v3/certs'
};

// 2. Login route - redirect to Google
app.get('/login', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  const nonce = crypto.randomBytes(16).toString('hex');

  req.session.oauthState = state;
  req.session.oauthNonce = nonce;

  const authUrl = new URL(GOOGLE_CONFIG.authorizationEndpoint);
  authUrl.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID);
  authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('scope', 'openid email profile');
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('nonce', nonce);

  res.redirect(authUrl.toString());
});

// 3. Callback route - exchange code for tokens
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(403).send('Invalid state');
  }

  // Exchange code for tokens
  const tokenResponse = await fetch(GOOGLE_CONFIG.tokenEndpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: process.env.GOOGLE_CLIENT_ID,
      client_secret: process.env.GOOGLE_CLIENT_SECRET,
      redirect_uri: 'https://yourapp.com/callback',
      grant_type: 'authorization_code'
    })
  });

  const tokens = await tokenResponse.json();

  // Validate ID token
  const user = await validateIdToken(tokens.id_token, req.session.oauthNonce);

  // Create or update user in database
  const dbUser = await upsertUser({
    googleId: user.sub,
    email: user.email,
    name: user.name,
    picture: user.picture
  });

  // Create session
  req.session.userId = dbUser.id;

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

OIDC vs SAML

Feature OIDC SAML
Format JSON (JWT) XML
Complexity Simple Complex
Mobile Support Excellent Poor
Use Case Modern apps, APIs Enterprise SSO

Key Takeaways

  • OIDC = OAuth + Identity - Standardized authentication layer
  • ID Token is a JWT - Contains verified user identity
  • Always validate ID tokens - Signature, issuer, audience, expiration
  • Use discovery - Auto-configure from .well-known/openid-configuration

Next up: Security Best Practices - Common vulnerabilities and how to prevent them.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is the relationship between OAuth 2.0 and OpenID Connect?

2

What does the 'openid' scope request?

3

What is the UserInfo endpoint used for?

Understanding Tokens