How OpenID Connect (OIDC) Works: Identity on Top of OAuth 2.0

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that lets applications verify a user's identity and obtain basic profile information. Every time you click "Sign in with Google" or "Continue with Microsoft" and the application learns your name, email, and profile picture, OpenID Connect is the protocol making that possible. While OAuth 2.0 handles authorization (granting an app access to your resources), OIDC adds authentication (proving who you are) by introducing a standardized identity token — the ID Token — that OAuth 2.0 deliberately left unspecified.

OIDC is defined by the OpenID Connect Core 1.0 specification (2014) and has become the dominant authentication protocol for both consumer and enterprise applications, largely replacing SAML in new deployments due to its simplicity and native mobile support.

Why OAuth 2.0 Was Not Enough

OAuth 2.0 was designed for authorization — granting a third-party application access to your resources (photos, repos, calendar) without sharing your password. But it deliberately did not define a way for the application to learn who you are. An OAuth 2.0 access token says "the bearer of this token may read the user's photos" — it does not say "the user is [email protected]."

Before OIDC, applications that wanted authentication via OAuth 2.0 would use the access token to call a provider-specific API endpoint (like GitHub's /user or Facebook's /me). Each provider had a different API, returned different fields, and required different request formats. This led to several problems:

OIDC solves all of these by adding a thin, standardized identity layer on top of OAuth 2.0.

The ID Token

The central artifact of OIDC is the ID Token — a JSON Web Token (JWT) that contains claims about the authentication event and the user. Unlike an OAuth 2.0 access token (which is opaque to the client), the ID Token is designed to be parsed and validated by the client application.

A decoded ID Token payload looks like this:

{
  "iss": "https://accounts.google.com",
  "sub": "110248495921384975384",
  "aud": "my-app-client-id.apps.googleusercontent.com",
  "exp": 1706461200,
  "iat": 1706457600,
  "nonce": "abc123def456",
  "auth_time": 1706457590,
  "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
  "email": "[email protected]",
  "email_verified": true,
  "name": "Jane Doe",
  "picture": "https://lh3.googleusercontent.com/..."
}
OIDC ID Token Claims iss Issuer — who created the token (IdP URL) sub Subject — unique, stable user identifier aud Audience — your app's client_id (MUST validate!) exp Expiration — token MUST be rejected after this time nonce Replay protection — ties token to specific auth request auth_time When the user actually authenticated (epoch seconds) at_hash Access token hash — binds ID token to access token Required Security Auth

The critical claims that every relying party must validate:

The ID Token is signed using the IdP's private key (typically RS256 or ES256), and the client verifies the signature using the IdP's public key obtained from the JWKS endpoint.

The Authorization Code Flow with PKCE

The recommended OIDC flow for most applications is the Authorization Code Flow with PKCE (Proof Key for Code Exchange, RFC 7636). This is the same OAuth 2.0 Authorization Code flow with two additions: the openid scope (which tells the authorization server to return an ID Token) and PKCE (which protects against authorization code interception).

OIDC Authorization Code Flow + PKCE User (Browser) Client App OpenID Provider (OP) Generate code_verifier + code_challenge 1. Click "Sign in" 2. 302 /authorize?scope=openid&code_challenge=... 3. User authenticates + consents 4. 302 redirect_uri?code=xyz&state=... 5. Forward auth code 6. POST /token (code + code_verifier) 7. { access_token, id_token, refresh_token } Validate ID Token (sig, iss, aud, nonce) 8. GET /userinfo (optional, with access_token) 9. { email, name, picture, ... } Front-channel Token exchange UserInfo

Step by step:

  1. PKCE setup — The client generates a random code_verifier (a 43-128 character string) and computes code_challenge = BASE64URL(SHA256(code_verifier)). The verifier is stored locally; the challenge is sent in the authorization request.
  2. Authorization request — The client redirects the user to the OpenID Provider's authorization endpoint with parameters including scope=openid email profile, response_type=code, client_id, redirect_uri, state (CSRF protection), nonce (replay protection), and code_challenge.
  3. User authentication — The OP authenticates the user (login form, MFA) and obtains consent. If the user already has an active session and has previously consented, this step can be instant.
  4. Authorization code — The OP redirects back to the client's redirect_uri with a short-lived authorization code and the original state value.
  5. Token exchange — The client sends the authorization code along with the code_verifier to the OP's token endpoint. The OP verifies that SHA256(code_verifier) matches the original code_challenge, confirming that the entity exchanging the code is the same one that initiated the flow.
  6. Tokens received — The OP returns an access token, an ID Token (JWT), and optionally a refresh token.
  7. ID Token validation — The client validates the ID Token's signature using the OP's JWKS keys, then verifies iss, aud, exp, and nonce claims.
  8. UserInfo (optional) — If the client needs additional user attributes not included in the ID Token, it calls the UserInfo endpoint with the access token.

PKCE: Why It Matters

PKCE (pronounced "pixy") was originally designed for public clients (mobile apps, SPAs) that cannot securely store a client secret. Without PKCE, an attacker who intercepts the authorization code (via a malicious app on the same device, browser history, or referrer header) could exchange it for tokens.

With PKCE, the authorization code is useless without the code_verifier, which never leaves the client application. The OP verifies that SHA256(code_verifier) == code_challenge before issuing tokens. Since the code_challenge is a one-way hash, an attacker who sees it cannot derive the verifier.

As of OAuth 2.1 (draft), PKCE is required for all clients, including confidential clients with client secrets. The reasoning is defense in depth: even confidential clients benefit from the additional protection against code injection and interception attacks.

Discovery Document

OIDC includes a standardized discovery mechanism. Every OpenID Provider publishes a JSON configuration document at a well-known URL:

https://accounts.google.com/.well-known/openid-configuration

This document contains everything a client needs to interact with the provider:

{
  "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", ...],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "claims_supported": ["sub", "email", "email_verified", "name", ...]
}

This is a major advantage over SAML, where metadata exchange is often a manual process involving XML file uploads. OIDC discovery makes multi-provider support trivial: the client just needs the issuer URL, and it can derive everything else automatically.

Scopes and Claims

OIDC defines standard scopes that map to sets of claims:

Claims can appear in the ID Token, the UserInfo response, or both. The OP decides which claims to include in the ID Token versus requiring a UserInfo call — Google, for example, includes email and name directly in the ID Token, while others require a separate UserInfo request.

The UserInfo Endpoint

The UserInfo endpoint is an OAuth 2.0 protected resource that returns claims about the authenticated user. The client calls it with the access token:

GET /userinfo HTTP/1.1
Host: openidconnect.googleapis.com
Authorization: Bearer ya29.a0AeDClZ...

Response:

{
  "sub": "110248495921384975384",
  "name": "Jane Doe",
  "given_name": "Jane",
  "family_name": "Doe",
  "picture": "https://lh3.googleusercontent.com/...",
  "email": "[email protected]",
  "email_verified": true
}

The sub claim in the UserInfo response must match the sub claim in the ID Token. If they differ, the response must be rejected — this prevents token substitution attacks.

Implicit Flow vs. Authorization Code Flow

The OIDC Implicit Flow returns the ID Token directly in the URL fragment (#id_token=...) without an intermediate authorization code. This was originally designed for SPAs (single-page applications) that could not make back-channel requests to the token endpoint.

The Implicit Flow is now considered insecure and is deprecated by the OAuth 2.0 Security Best Current Practice (RFC 9700). Problems include:

Modern SPAs should use the Authorization Code Flow with PKCE. The client makes a cross-origin POST to the token endpoint (which CORS enables) and receives tokens in a response body rather than a URL fragment.

Nonce Validation

The nonce parameter is OIDC's primary defense against replay attacks. The flow works like this:

  1. The client generates a cryptographically random nonce value and stores it (in the session, a cookie, or local storage).
  2. The client includes the nonce in the authorization request.
  3. The OP includes the same nonce value in the ID Token.
  4. When the client receives the ID Token, it verifies that the nonce claim matches the value it stored earlier.

If an attacker captures an ID Token and tries to replay it, the client will reject it because the nonce will not match the current session's expected value. This is conceptually similar to CSRF tokens but applied to the authentication flow.

Nonce validation is required when using the Implicit Flow (because the ID Token passes through the browser) and strongly recommended for the Authorization Code Flow.

Token Validation

Validating an ID Token properly is critical for security. The full validation procedure is:

  1. Decode the JWT — Split on dots, Base64url-decode header and payload.
  2. Verify the signature — Fetch the OP's public keys from the JWKS endpoint (cached locally). The JWT header's kid identifies which key to use. Verify the signature using the corresponding public key.
  3. Validate iss — Must exactly match the issuer URL from the discovery document.
  4. Validate aud — Must contain your client_id. If aud is an array, also check that an azp (authorized party) claim is present and matches your client_id.
  5. Validate exp — Current time must be before the expiration. Allow a small clock skew tolerance.
  6. Validate iat — Reject tokens issued too far in the past.
  7. Validate nonce — Must match the nonce you sent in the authorization request.
  8. Validate at_hash (if present) — Compute the hash of the access token and verify it matches. This binds the ID Token to the access token, preventing token substitution.

Libraries like jsonwebtoken (Node.js), PyJWT (Python), and golang-jwt (Go) handle steps 1-2. Steps 3-8 are application logic that you must implement correctly.

Dynamic Client Registration

OIDC defines an optional Dynamic Client Registration protocol (RFC 7591) that allows clients to register programmatically without manual configuration at the OP. The client sends a POST request to the registration endpoint with its metadata:

POST /register HTTP/1.1
Content-Type: application/json

{
  "application_type": "web",
  "redirect_uris": ["https://myapp.example.com/callback"],
  "client_name": "My Application",
  "token_endpoint_auth_method": "none"
}

The OP responds with a client_id (and possibly a client_secret). This enables automated onboarding of new applications without administrator intervention, which is useful in large organizations or platform-as-a-service environments.

Session Management

OIDC extends beyond initial authentication to address session lifecycle:

OIDC vs. SAML: When to Use Which

Both OIDC and SAML provide federated SSO, but they target different environments:

Security Considerations

Common OIDC implementation pitfalls:

OIDC and Network Infrastructure

Like all web protocols, OIDC depends on the network stack operating correctly:

Relevant RFCs and Specifications

See It in Action

Major OpenID Providers operate infrastructure routed via BGP. Explore their networks using the god.ad BGP Looking Glass:

See BGP routing data in real time

Open Looking Glass
More Articles
How TLS/HTTPS Works: Securing the Internet's Traffic
Certificate Transparency: How CT Logs Secure the Web's PKI
How Firewalls Work: Packet Filtering, Stateful Inspection, and Beyond
What is Cross-Site Scripting (XSS)?
What is Cross-Site Request Forgery (CSRF)?
What is Server-Side Request Forgery (SSRF)?