OAuthOIDCID tokenidentityfederationSSOclaims

OpenID Connect

OAuth plus an identity layer: an ID token issued to your client, signed by the provider, containing verified claims about who the user is — which is the piece OAuth deliberately does not provide.

▶ Run the labFollow the failure

Frame the problem

Security starts with a concrete asset, attacker capability and trust crossing.

Asset
The identity assertion — the claim that this browser session belongs to a specific, verified person.
Attacker & capability
Someone replaying an ID token obtained elsewhere, or exploiting a client that accepts identity claims without verifying who issued them and for whom.
Trust boundary
The boundary between the identity provider's assertion and your application's session — crossed exactly once, at login, and absolutely dependent on correct verification.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

What the identity layer adds

OAuth gets you an access token for an API. OIDC adds three things on top, and each one closes a specific gap that made "log in with OAuth" unsafe.

An ID token: a JWT issued *to your client*, with your client id in aud, signed by the provider, containing sub (a stable identifier for the user at this provider), iss, exp, and the nonce from your authorization request. Because it is addressed to you and bound to your request, a token obtained by a different application is not usable at yours — which is exactly the flaw in authenticating with a bare access token.

A UserInfo endpoint for claims that do not belong in the token, and standard scopes (openid, profile, email) with defined claim names, so integrating a second provider does not mean learning a second data model.

And discovery: a well-known configuration document naming the endpoints and the JWKS URI, so key rotation is automatic and clients do not hardcode certificates.

The distinction, precisely
OAuth 2.xOpenID Connect
Question answeredMay this app act on the user's behalf?Who is this user?
Primary artefactAccess tokenID token (plus an access token if needed)
AudienceThe resource serverYour client, specifically
Verifiable by you?No — it is not addressed to youYes — signature, iss, aud, nonce
Identity claimsNone defined by the specsub, email, name, … with standard names
Correct useCalling a third-party APILogging a user into your application

Verifying an ID token, and the identifier to key on

The verification is the JWT checklist from JWT Failure Modes with two additions specific to OIDC: aud must be your client id, and nonce must match the value you generated for this authorization request. The nonce is what prevents a previously-obtained ID token from being replayed into a fresh login.

The subtler decision is which claim identifies the user in your database. The answer is iss + sub together, not email. Email addresses change, can be reassigned by an organisation after an employee leaves, and — critically — may be unverified. A provider that returns email without email_verified: true is telling you the user typed it, not that they control it. Keying accounts on an unverified email is an account-takeover primitive: register at the identity provider with someone else's address, log in to the relying application, and inherit their account.

The related trap is account linking. When a user who signed up with a password later logs in with a provider that reports the same email, linking the two automatically is convenient and unsafe unless the email is verified by both sides. The safe pattern is to require the user to authenticate with the existing method before linking, or to require a verified email on both sides and log the link as a security event.

1const { payload } = await jwtVerify(idToken, jwksForIssuer(ISSUER), {
2 algorithms: ['RS256'],
3 issuer: ISSUER, // exactly the configured issuer
4 audience: CLIENT_ID, // this token was minted FOR US
5 maxTokenAge: '5m', // id_tokens are consumed immediately at login
6})
7if (payload.nonce !== saved.nonce) throw new AuthError('nonce mismatch') // replay defence
8
9// Key on issuer + subject. Never on email alone.
10let account = await accounts.byFederatedId(payload.iss, payload.sub)
11
12if (!account) {
13 // Linking to an existing local account requires a verified address AND,
14 // for anything sensitive, proof that the user controls that account already.
15 if (payload.email && payload.email_verified === true) {
16 const existing = await accounts.byEmail(payload.email)
17 if (existing) {
18 if (!(await proveControlOfExistingAccount(existing))) throw new LinkRequiresVerification()
19 await accounts.link(existing.id, payload.iss, payload.sub)
20 await audit.log('account.federated.linked', { accountId: existing.id, iss: payload.iss })
21 account = existing
22 }
23 }
24 account ??= await accounts.createFederated(payload)
25}
26return establishSession(account.id)

Federation and its consequences

Adopting OIDC means outsourcing authentication, and the trade should be explicit. What you gain: MFA, breached-password detection, device management, anomaly detection and session policy, all maintained by someone whose job it is — usually far better than you would build. What you take on: a hard dependency on the provider's availability, and a trust relationship where the provider can assert *any* identity in its namespace.

That second point has an operational shape in enterprise SSO. The identity provider administrator can, by definition, sign in as any user in the organisation. That is the intended design, and it means the provider's admin accounts are as privileged as your most privileged user. It also means deprovisioning must actually work: an employee disabled in the identity provider still holds any session your application issued, and any API token they created, until you revoke them. Provisioning protocols and back-channel logout exist for this; if you do not implement one, write down that offboarding is asynchronous and how long the window is.

Finally, plan for provider unavailability. Login is the front door; if the provider is down, nobody gets in. Whether you keep a break-glass local-credential path for administrators is a real decision with real risk on both sides — an emergency account is also a permanent attack target — and it should be made deliberately, protected with a hardware factor, and audited on every use.

Key points

  • OIDC adds an ID token addressed to your client, which is what makes federated login verifiable.
  • Verify signature, iss, aud = your client id, exp, and nonce against the value you generated.
  • Key accounts on iss + sub, never on email — and never link accounts on an unverified address.
  • Federation outsources authentication quality and takes on a hard availability and trust dependency.
  • Deprovisioning is not automatic: sessions and API tokens outlive the disabled identity-provider account unless you revoke them.

Boundary control exercise

This lesson uses the shared boundary-control exercise.

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Follow the attack

Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.

  1. 1
    Attacker → register at a provider using a victim's email address, where the provider does not verify it.
  2. 2
    Unverified email → log in to a relying application that keys accounts on email.
  3. 3
    Account match → inherit the victim's account without ever touching their credential.
  4. 4
    Alternatively → replay an ID token obtained elsewhere at a client that does not check `aud` or `nonce`.
Blast radius
  • Account takeover through email-based linking, with no credential compromise anywhere.
  • Cross-client token replay where audience is unchecked.
  • Continued access after offboarding, for as long as sessions and tokens survive.

Defend, detect, recover

One prevention is a single point of security failure. Layer it and make failure observable.

Prevent
  • • Require `email_verified` before any email-based linking, and require proof of control of the existing account.
  • • Verify `aud` and `nonce` on every ID token; use a maintained OIDC library.
  • • Implement back-channel logout or directory-driven deprovisioning, and revoke sessions and API tokens on disable.
  • • Treat identity-provider administrators as top-tier privileged accounts with hardware-key MFA.
Detect
  • • Alert on account linking events, especially where the local account predates the federated identity.
  • • Alert on ID tokens rejected for audience or nonce mismatch.
  • • Alert on successful authentication for an identity that was disabled in the directory.
Respond & recover
  • • Unlink the fraudulent federated identity and revoke all sessions for the affected account.
  • • Audit all links created by the same provider identity or email address.
  • • Force re-verification of the account owner through an independent channel.
Residual risk
  • • The identity provider can assert any identity in its namespace; that trust is the design.
  • • Provider outage is a total login outage unless a break-glass path exists — and that path is itself a target.
  • • Deprovisioning lag is real in every implementation; the question is only how long.

Misconceptions

Claim
“Social login is inherently less secure.”
Reality
A major provider almost certainly runs better authentication than you would build. The risk moves to linking, deprovisioning and dependency, not to the credential.
Claim
“Disabling the user in the identity provider logs them out.”
Reality
Only if your application implements back-channel logout or checks the directory. Otherwise existing sessions and API tokens continue to work.