Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OpenID Connect (OIDC)

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0, standardized by the OpenID Foundation in 2014 ( OIDC Core 1.0 ). It adds an ID Token (a signed JWT) to the OAuth 2.0 authorization flow, providing authentication in addition to OAuth’s authorization. This page covers the protocol, the ID Token structure, the discovery and metadata endpoints, and the relationship between OIDC and OAuth 2.0.

The OIDC vs OAuth 2.0 Distinction

OAuth 2.0 (RFC 6749) is an authorization framework: it defines how a client obtains an access token that grants permission to access a resource. It does not define how the resource server authenticates the user.

OIDC adds authentication on top:

  • OAuth 2.0: client gets access_token → can call API as user.
  • OIDC: client gets access_token + id_token → can call API as user AND know who the user is.

The id_token is a JWT (JSON Web Token) signed by the OpenID Provider (OP), containing claims about the user (subject, email, name, etc.).

The Authorization Code Flow with PKCE

The most common OIDC flow for web apps and mobile apps:

1. Client → OP: Redirect to https://op.example.com/authorize
   Query:  response_type=code
            client_id=...
            redirect_uri=https://app.example.com/callback
            scope=openid profile email
            state=<random>
            nonce=<random>
            code_challenge=<hash-of-verifier>
            code_challenge_method=S256

2. User authenticates to OP (e.g., password + MFA).

3. OP → Client: Redirect to redirect_uri
   Query:  code=<authorization-code>
            state=<echoed-from-step-1>

4. Client → OP: POST https://op.example.com/token
   Body:   grant_type=authorization_code
            code=<code>
            redirect_uri=<echoed>
            client_id=...
            code_verifier=<verifier>

5. OP verifies code + code_verifier (must match the challenge).
   OP returns:
   {
     "access_token": "...",
     "id_token": "<JWT>",
     "refresh_token": "...",  // optional, if offline_access scope
     "expires_in": 3600,
     "token_type": "Bearer"
   }

The code_challenge / code_verifier pair (PKCE, RFC 7636) prevents an attacker who intercepts the authorization code (e.g., via a malicious app on the same device) from exchanging it for tokens. The client sends code_challenge = SHA256(code_verifier) in step 1; the OP requires code_verifier in step 4 and validates that SHA256(code_verifier) == code_challenge.

PKCE is mandatory for mobile and SPA clients (per OAuth 2.1 draft) and recommended for all clients.

The ID Token

The id_token is a JWT with three parts:

eyJhbGciOiJSUzI1NiIsImtpZCI6Ik...  ← header
.eyJpc3MiOiJodHRwczovL29wLmV4...  ← payload (claims)
.SflKxwRJSMeKKpadsTiopQRSTUVWXYZ  ← signature

Decoded header:

{
  "alg": "RS256",
  "kid": "key-1",
  "typ": "JWT"
}

Decoded payload (the ID Token claims):

{
  "iss": "https://op.example.com",
  "sub": "alice123",
  "aud": "client-123",
  "exp": 1692620400,
  "iat": 1692616800,
  "nonce": "abc123",
  "auth_time": 1692616700,
  "acr": "urn:mace:incommon:iap:silver",
  "amr": ["mfa", "pwd"],
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Adams",
  "picture": "https://op.example.com/alice.png"
}

Standard OIDC claims:

ClaimMeaning
issIssuer URL (the OP’s identifier)
subSubject identifier (unique per user per OP)
audAudience (the client_id of the intended recipient)
expExpiration time (Unix timestamp)
iatIssued at time
nonceEchoed from step 1; prevents replay
auth_timeWhen the user authenticated (Unix timestamp)
acrAuthentication Context Class Reference (level of auth)
amrAuthentication Methods References (e.g., [“mfa”, “pwd”])
emailUser’s email
email_verifiedWhether the email was verified
nameDisplay name
pictureAvatar URL

The ID Token is signed by the OP using its private key (RS256 is the most common algorithm). Clients verify the signature using the OP’s public key, obtained from the OP’s discovery endpoint.

Discovery and JWKS

OIDC defines a standard discovery mechanism. The OP publishes its configuration at https://op.example.com/.well-known/openid-configuration:

{
  "issuer": "https://op.example.com",
  "authorization_endpoint": "https://op.example.com/authorize",
  "token_endpoint": "https://op.example.com/token",
  "userinfo_endpoint": "https://op.example.com/userinfo",
  "jwks_uri": "https://op.example.com/.well-known/jwks.json",
  "response_types_supported": ["code", "id_token"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "subject_types_supported": ["public", "pairwise"],
  "id_token_signing_alg_values_supported": ["RS256", "ES256"],
  "scopes_supported": ["openid", "profile", "email", "offline_access"]
}

Clients fetch this document once at startup and use it to discover the OP’s endpoints and supported algorithms.

The jwks_uri points to the OP’s JSON Web Key Set (JWKS) — a JSON document containing the public keys the OP uses to sign tokens:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-1",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "key-2",
      "use": "sig",
      "alg": "RS256",
      "n": "...",
      "e": "AQAB"
    }
  ]
}

Each ID Token’s kid header identifies which key was used; the client fetches the JWKS and finds the matching key.

Key Rotation

Production OPs rotate signing keys periodically (e.g., monthly). The rotation process:

  1. OP generates a new key pair.
  2. OP adds the new public key to JWKS (alongside the old key).
  3. OP starts signing new ID Tokens with the new key.
  4. OP removes the old key from JWKS after all issued ID Tokens with the old key have expired (typically 24 hours).

Clients must re-fetch the JWKS periodically (e.g., every hour) to pick up new keys. If a client has a stale JWKS cache and tries to validate a token signed with a new key, validation fails. Most OIDC libraries handle this automatically.

The Userinfo Endpoint

The ID Token contains claims, but the user’s profile data may change between token issuance and use. The userinfo_endpoint is a REST endpoint where the client can exchange an access token for the current user’s claims:

GET /userinfo HTTP/1.1
Host: op.example.com
Authorization: Bearer <access_token>

Response (JSON):

{
  "sub": "alice123",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Adams",
  "picture": "https://op.example.com/alice.png"
}

The userinfo endpoint is OPTIONAL per OIDC Core. Many OPs provide it; many apps use the ID Token’s claims directly without calling userinfo (the ID Token is signed, so its claims are trusted; userinfo is for fresh data).

The Implicit Flow (Deprecated)

OIDC also defined the “Implicit Flow” where the ID Token is returned directly in the redirect URL (no code exchange). This was designed for browser-only SPAs that couldn’t keep a secret. With PKCE now mandatory and refresh tokens being safe to store in SPAs, the Implicit Flow is deprecated in OAuth 2.1 and the OIDC Best Current Practice.

OIDC vs SAML

AspectOIDCSAML 2.0
EncodingJSON / JWTXML
Discovery.well-known/openid-configurationMetadata URL or out-of-band
Browser flowAuthorization Code + PKCEWeb Browser SSO
API authBearer tokensAwkward (SAMLBearer)
Mobile appsExcellent (PKCE)Awkward
Enterprise deploymentNewer, growingMature, dominant in enterprise
IdP-initiated SSOAwkwardNative
Modern librariesJSON-native (everywhere)XML-heavy

For new deployments, OIDC is the recommended choice. For existing enterprise SSO, SAML remains dominant.

Production OpenID Providers

  • Auth0 (Okta): cloud-hosted, dev-friendly.
  • Keycloak (Red Hat): open-source, Java-based.
  • Okta: cloud-hosted, enterprise-focused.
  • Microsoft Entra ID (formerly Azure AD): Microsoft’s cloud IdP.
  • Google Identity Platform: cloud-hosted.
  • Authentik (open-source, Python): newer alternative to Keycloak.
  • Ory (open-source, Go): cloud-native, more opinionated.

Common Pitfalls

  1. Not using PKCE. Without PKCE, an attacker who intercepts the authorization code (e.g., via a malicious app’s custom URL handler on mobile) can exchange it for tokens. Always use PKCE.

  2. Verifying only the JWT signature, not the claims. A token signed by the OP is “authentic”, but if aud doesn’t match your client_id or exp is in the past, it’s not valid for your app.

  3. Forgetting to validate nonce. The nonce claim in the ID Token must match the nonce you sent in step 1. Without this, a token issued for a different session can be replayed.

  4. Using id_token for API authorization. The ID Token is for the client to identify the user; it’s not for the resource server. Use the access_token for API calls.

  5. Trusting claims without email_verified. The email claim may be the user’s email but is not verified unless email_verified: true. Don’t trust unverified emails for password resets or sensitive operations.

  6. Storing access tokens in localStorage. SPAs that store tokens in localStorage are vulnerable to XSS attacks that exfiltrate tokens. Use HttpOnly cookies with backend-mediated OAuth, or follow the BCP for SPA token handling.

References