OIDC

OpenID Connect - an identity layer on top of OAuth 2.0

What is OIDC?

OpenID Connect (OIDC) is a thin identity layer built on top of OAuth 2.0. OAuth 2.0 answers authorisation - can this app access this resource on your behalf? OIDC answers authentication- who are you? It does this by adding one thing OAuth doesn't have: a standardised ID token.

In practice: when you implement "Sign in with Google" or "Sign in with GitHub", you are using OIDC. The underlying OAuth flow handles the token exchange; OIDC standardises what the token contains and how identity is conveyed.

OAuth 2.0 vs OIDC - the distinction

OAuth 2.0 issues an access token - an opaque credential that grants access to a resource. It says nothing about who the user is. You can use an access token to call an API; you cannot reliably use it to log someone in.

OIDC adds an ID token - a JWT issued by the identity provider that contains verified claims about the user. Your application validates this token and uses the claims to establish a session.

The analogy

OAuth is a valet key - it grants limited access to something. OIDC is a passport - it proves who you are. You need the passport (OIDC) to log in; you need the valet key (OAuth) to act on behalf of someone.

Key concepts

  • Identity Provider (IdP) - the service that authenticates the user and issues the ID token. Google, GitHub, Auth0, Okta, Cognito are all IdPs.
  • Relying Party (RP) - your application. It trusts the IdP to vouch for the user and consumes the ID token.
  • ID token - a signed JWT containing claimsabout the user. Your app validates the signature against the IdP's public key before trusting anything in it.
  • UserInfo endpoint- an API endpoint the IdP exposes. You can call it with an access token to retrieve additional claims that weren't included in the ID token.
  • Scope: openid - requesting this scope in the OAuth flow is what turns it into an OIDC flow. Without openid in the scope, you get an access token but no ID token.

The ID token

A decoded OIDC ID token payload looks like this:

{
  "iss": "https://accounts.google.com",   // issuer - who signed this
  "sub": "1234567890",                    // subject - stable unique user ID
  "aud": "your-client-id",               // audience - must match your app
  "exp": 1718000000,                      // expiry - unix timestamp
  "iat": 1717996400,                      // issued at
  "email": "user@example.com",
  "email_verified": true,
  "name": "Jane Smith",
  "picture": "https://..."
}
  • iss - validate this matches the expected IdP.
  • sub - use this as the stable identifier for the user in your database, not email. Emails change; sub doesn't.
  • aud - validate this matches your client ID. Prevents token reuse across apps.
  • exp - validate the token hasn't expired.

The flow (Authorization Code + PKCE)

OIDC uses the OAuth 2.0 Authorization Code flow. With PKCE (Proof Key for Code Exchange) this is safe for public clients (SPAs, mobile apps) as well as server-side apps.

  1. User clicks "Sign in with Google". Your app redirects to the IdP's authorisation endpoint with scope=openid email profile and a state parameter.
  2. User authenticates at the IdP and grants consent.
  3. IdP redirects back to your redirect_uri with a short-lived code.
  4. Your server exchanges the code for tokens at the IdP's token endpoint (server-to-server, never exposed to the browser).
  5. IdP returns an access token and an ID token.
  6. Your server validates the ID token signature and claims, then creates a session.

The state parameter

Always generate a random statevalue, store it in the session before redirecting, and verify it matches when the IdP redirects back. This prevents CSRF attacks on the OAuth callback - an attacker can't forge a redirect with a code they initiated.

Validating the ID token

Never trust an ID token without validating it. The IdP publishes its public keys at a well-known URL (/.well-known/openid-configurationjwks_uri). Fetch those keys and verify the JWT signature against them.

# Python - using PyJWT
import jwt
from jwt import PyJWKClient

jwks_client = PyJWKClient("https://accounts.google.com/.well-known/openid-configuration")
signing_key = jwks_client.get_signing_key_from_jwt(id_token)

payload = jwt.decode(
    id_token,
    signing_key.key,
    algorithms=["RS256"],
    audience="your-client-id",
    issuer="https://accounts.google.com",
)
# payload["sub"] is now your verified user identifier

In practice, use a library or an established OIDC client package rather than doing this manually. python-jose, authlib, and django-allauth handle token validation, key rotation, and clock skew for you.

Discovery document

Every OIDC-compliant IdP exposes a discovery document at <issuer>/.well-known/openid-configuration. It lists all the endpoints, supported scopes, signing algorithms, and the jwks_uri for public keys. You rarely need to hardcode endpoint URLs - fetch the discovery document instead.

# Google's discovery document
GET https://accounts.google.com/.well-known/openid-configuration

# Returns JSON including:
{
  "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",
  ...
}

Key points

  • OIDC = OAuth 2.0 + identity. Request openid scope to get an ID token.
  • Use sub as the user identifier in your database - not email.
  • Always validate the ID token signature, iss, aud, and exp.
  • Always validate the state parameter on callback to prevent CSRF.
  • Use a library. Token validation has enough edge cases (key rotation, clock skew) that rolling your own is risky.