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. Withoutopenidin 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;subdoesn'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.
- User clicks "Sign in with Google". Your app redirects to the IdP's authorisation endpoint with
scope=openid email profileand astateparameter. - User authenticates at the IdP and grants consent.
- IdP redirects back to your
redirect_uriwith a short-livedcode. - Your server exchanges the
codefor tokens at the IdP's token endpoint (server-to-server, never exposed to the browser). - IdP returns an access token and an ID token.
- 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-configuration → jwks_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 identifierIn 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
openidscope to get an ID token. - Use
subas the user identifier in your database - not email. - Always validate the ID token signature,
iss,aud, andexp. - Always validate the
stateparameter on callback to prevent CSRF. - Use a library. Token validation has enough edge cases (key rotation, clock skew) that rolling your own is risky.