privileged.id

OpenID Connect

OpenID Connect

The identity layer on OAuth 2.0: what the ID token proves, how to validate it, and how to identify users safely.

Reviewed 7 min read 5 primary sources

OpenID Connect (OIDC) is the identity layer on top of OAuth 2.0. OAuth lets a client get an access token for an API; OpenID Connect lets the client learn, in a verifiable way, which user signed in, at which provider, when, and how. It does this with one addition, the ID token, plus a standard UserInfo endpoint and discovery metadata. The core specification is OpenID Connect Core 1.0, currently published incorporating errata set 2 (December 2023).

OIDC uses its own vocabulary for OAuth roles: the client is the Relying Party (RP) and the authorization server is the OpenID Provider (OP).

How it sits on top of OAuth

An OIDC request is an OAuth authorization request with openid in the scope. That scope value is what turns it into an authentication request; without it, OIDC Core says "the behavior is entirely unspecified". The provider authenticates the user and, in addition to any access token, returns an ID token addressed to the client.

OpenID Connect authorization code flow Browser Relying party the client OpenID provider OAuth AS 1. 302: scope=openid, nonce 2. GET /authorize (code flow + PKCE) 3. Authenticate the user 4. 302 to redirect_uri with code 5. GET /cb?code&state 6. POST /token + code_verifier 7. ID token + access token 8. Validate the ID token 9. GET /userinfo (optional)
The authorization code flow: identical to OAuth's, with scope=openid, a nonce, and an ID token in the token response that the RP must validate before trusting.
GET /authorize?response_type=code
  &client_id=s6BhdRkqt3
  &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
  &scope=openid%20email
  &state=af0ifjsldkj
  &nonce=n-0S6_WzA2Mj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256 HTTP/1.1
Host: server.example.com

In OIDC the redirect_uri is REQUIRED and MUST exactly match a pre-registered value by simple string comparison (§3.1.2.1). The token response then carries an id_token next to the access token:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "SlAV32hkKG",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "8xLOxBtZp8",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ..."
}

The ID token

The ID token is a JWT that MUST be signed, and may additionally be encrypted (signed first, then encrypted). The non-normative example from OIDC Core §2:

{
  "iss": "https://server.example.com",
  "sub": "24400320",
  "aud": "s6BhdRkqt3",
  "nonce": "n-0S6_WzA2Mj",
  "exp": 1311281970,
  "iat": 1311280970,
  "auth_time": 1311280969,
  "acr": "urn:mace:incommon:iap:silver"
}
ClaimStatusMeaning
issREQUIREDIssuer identifier: a case-sensitive https URL with no query or fragment.
subREQUIREDSubject identifier, "locally unique and never reassigned within the Issuer", at most 255 ASCII characters.
audREQUIREDMUST contain the RP's client_id; may contain other audiences.
expREQUIREDMust not be accepted on or after this time. Unrelated to how long the user's session lasts.
iatREQUIREDWhen the token was issued.
auth_timeRequired if max_age was sent or it was requested as essential; else optionalWhen the user actually authenticated (which may be long before iat if an existing session was reused).
noncePresent if sent in the requestThe value from the request, passed through unmodified. If the request had one, the OP MUST include it.
acrOptionalAuthentication context class the authentication satisfied. Its meaning must be agreed between the parties.
amrOptionalArray of authentication method identifiers (for example password plus OTP), preferably from the IANA registry created by RFC 8176.
azpOptionalAuthorized party. Errata set 2 notes it only occurs with extensions and encourages implementations not using them to ignore it.

The acr and amr claims are how an RP learns how the user signed in, for example whether a phishing-resistant authenticator was used. They are only as meaningful as the agreement behind them; check the provider's documentation for the values it emits.

ID token validation (§3.1.3.7)

For the authorization code flow, OIDC Core §3.1.3.7 lists thirteen steps. In summary:

  1. If the ID token is encrypted, decrypt it with the keys and algorithms agreed at registration. If encryption was agreed and the token is not encrypted, the RP SHOULD reject it.
  2. The issuer identifier (usually from discovery) MUST exactly match iss.
  3. aud MUST contain the RP's client_id. Reject the token if it does not, or if it lists additional audiences the RP does not trust.
  4. If extensions put an azp claim in the token, the RP SHOULD validate it as those extensions specify.
  5. That validation MAY include checking that azp, when present, is the RP's own client_id.
  6. Because the code flow receives the ID token directly from the token endpoint over TLS, server authentication of that TLS connection MAY be used to validate the issuer in place of checking the signature. All other ID tokens' signatures MUST be validated with the issuer's keys.
  7. The alg SHOULD be RS256 (the default) or the algorithm the client registered.
  8. For MAC algorithms (HS256 and so on), the key is the UTF-8 bytes of the client secret.
  9. The current time MUST be before exp.
  10. iat can be used to reject tokens issued too far from the current time; the window is the RP's choice.
  11. If a nonce was sent, the claim MUST be present and equal to it. The RP SHOULD check for nonce replay.
  12. If acr was requested, the RP SHOULD check the value is appropriate.
  13. If auth_time was requested (directly or with max_age), the RP SHOULD check it and request re-authentication if too much time has passed.

If the client uses the nonce as its defense against authorization code injection instead of PKCE (allowed only for confidential clients), RFC 9700 §4.5.3.2 adds two MUSTs: validate the nonce in the ID token from the token endpoint even if another ID token arrived in the authorization response, and use none of the tokens until that check succeeds.

Flows: code, implicit, hybrid

The response_type selects one of three flows (§3):

Flowresponse_typeTokens fromStatus today
Authorization codecodeToken endpoint onlyUse this, with PKCE, for all clients.
Implicitid_token, id_token tokenAuthorization endpoint, in the URL fragmentAvoid. id_token token puts an access token in the front channel, which RFC 9700 §2.1.2 says clients SHOULD NOT do. nonce is REQUIRED here.
Hybridcode id_token, code token, code id_token tokenBothcode id_token is acceptable to RFC 9700 because the access token still comes from the token endpoint; variants containing token are not.

OIDC Core's own comparison table shows why the code flow wins: it is the only flow in which no token is revealed to the user agent, and, like the hybrid flow, it lets the client be authenticated and receive a refresh token. There is no remaining reason for new single-page apps to use the implicit flow; they use the code flow with PKCE, as described on authorization code with PKCE.

The UserInfo endpoint

UserInfo is an OAuth protected resource on the provider that returns claims about the user who authorized the access token (§5.3). It MUST use TLS and accept access tokens as RFC 6750 bearer tokens; GET with the Authorization header is RECOMMENDED.

GET /userinfo HTTP/1.1
Host: server.example.com
Authorization: Bearer SlAV32hkKG

HTTP/1.1 200 OK
Content-Type: application/json

{
  "sub": "248289761001",
  "name": "Jane Doe",
  "email": "janedoe@example.com",
  "email_verified": true
}

The sub in the UserInfo response MUST be checked to exactly match the sub in the ID token; if they differ, the response MUST NOT be used (§5.3.2). This guards against token substitution, where the access token belongs to a different user than the ID token.

Discovery and JWKS

An OP publishes its configuration at /.well-known/openid-configuration under its issuer URL (OpenID Connect Discovery 1.0 §4). The document lists the endpoints, supported scopes, response types, signing algorithms, and jwks_uri, which is REQUIRED and MUST use https. Validation rules:

  • The issuer returned MUST be identical to the URL used as the prefix to fetch the document, and identical to the iss in ID tokens from that issuer (Discovery §4.3). If validation fails, the information MUST NOT be used.
  • Fetch signing keys only from jwks_uri. OIDC Core says ID tokens SHOULD NOT carry x5u, x5c, jku or jwk headers; keys are communicated in advance through discovery and registration.
  • Cache the JWKS, and handle rotation by refetching when an unknown kid appears (see JWT).

Identifying users: iss + sub, never email

This is the most consequential rule in OIDC for account security, from §5.7:

Therefore, the only guaranteed unique identifier for a given End-User is the combination of the iss Claim and the sub Claim.

  • sub is unique only within an issuer. Two providers can both issue sub: "24400320" for different people. Key your account table on the pair (iss, sub).
  • Email, phone number, preferred username and name MUST NOT be used as unique identifiers. An issuer MAY reassign an email address to a different user over time, and a user's email can change.
  • Linking a federated login to an existing local account by matching email is how account takeovers happen: someone who controls that address at any accepted issuer, or who can register it unverified, gets the account. If you must link by email, require email_verified: true from an issuer you trust for that domain, and prefer an explicit linking step while the user is signed in to the existing account.
  • Some providers issue pairwise sub values, different per RP, so the same person has different identifiers at different clients (OIDC Core §8). Do not try to correlate them.

Checklist

  • Authorization code flow with PKCE and S256; state and a per-request nonce.
  • Configuration from discovery; issuer checked against the URL and against iss.
  • ID token validated per §3.1.3.7, with an explicit algorithm allow-list.
  • Users keyed on (iss, sub); no account linking on unverified email.
  • UserInfo sub compared with the ID token sub.
  • ID tokens never sent to APIs as access tokens; access tokens never treated as proof of login.
  • acr/amr/auth_time checked where the application needs a particular strength or freshness of authentication.
  • After validation, the application creates its own session; the ID token's exp is not the session lifetime.

Sources

  1. OpenID Connect Core 1.0 incorporating errata set 2, OpenID Foundation. OpenID Foundation specification (cited, brief quotations).
  2. OpenID Connect Discovery 1.0 incorporating errata set 2, OpenID Foundation. OpenID Foundation specification (cited, brief quotations).
  3. RFC 9700: Best Current Practice for OAuth 2.0 Security, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  4. RFC 6749: The OAuth 2.0 Authorization Framework, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  5. RFC 7519: JSON Web Token (JWT), IETF. IETF Trust Legal Provisions (brief quotations with attribution).