Authentication And Identity Systems

OAuth And OpenID Connect

The OAuth Roles And Flow

Four roles: the resource owner (the user), the client (your app), the authorization server (Google's login), and the resource server (Google's API). The modern flow for a web app is the authorization code flow, and its shape is worth internalizing:

PHP example
<?php

declare(strict_types=1);

$steps = [
    '1' => 'app redirects user to the authorization server with client_id, scope, state',
    '2' => 'user authenticates there and consents',
    '3' => 'auth server redirects back with a short-lived authorization code',
    '4' => 'app exchanges the code (plus its secret) server-to-server for tokens',
    '5' => 'app receives an access token, and with OIDC, an ID token',
];

foreach ($steps as $n => $step) {
    echo $n . '. ' . $step . PHP_EOL;
}

// Prints:
// 1. app redirects user to the authorization server with client_id, scope, state
// 2. user authenticates there and consents
// 3. auth server redirects back with a short-lived authorization code
// 4. app exchanges the code (plus its secret) server-to-server for tokens
// 5. app receives an access token, and with OIDC, an ID token

The reason the code is exchanged server-to-server in step 4, rather than tokens being handed straight to the browser, is that the browser is an untrusted, inspectable environment. The code alone is useless without the client secret, so intercepting the redirect gains an attacker nothing. Public clients that have no secret (mobile apps, SPAs) close the same gap with PKCE, a proof-of-possession that binds the code to the requester; PKCE is now recommended for confidential web clients too.

The Two Non-Negotiable Guards

Two parameters prevent the two classic OAuth attacks, and skipping either is how real breaches happen.

The state parameter is a random value your app generates, sends in step 1, and verifies on the callback. It ties the callback to the session that started the flow, defeating CSRF against the redirect — without it, an attacker can trick a logged-in user into completing the attacker's authorization flow and linking the attacker's identity into the victim's session.

Token validation is the other. An access token is a bearer credential — whoever holds it can use it — so it must reach your server only over TLS and be treated as a secret. But the ID token, being the authentication claim, must be validated, not merely received: verify its signature against the provider's published keys, and check the issuer, the audience (it was minted for your client_id), and the expiry. The dangerous shortcut is decoding an ID token and trusting its email claim without validation — an attacker supplies a token minted for a different app, or an unsigned one, and logs in as anyone.

OpenID Connect Specifics

OIDC adds the ID token (a signed JWT describing the user — subject, email, name), the userinfo endpoint (the same claims fetched with the access token), and a discovery document (/.well-known/openid-configuration) that publishes the provider's endpoints and signing keys so libraries configure themselves. The sub claim is the stable, unique user identifier — always key your local account off sub, never off email, because emails change hands and change addresses, and matching on email is the root of the account-linking vulnerabilities in the next lesson.

Use a mature library rather than hand-rolling any of this. The flows have sharp edges — nonce checking, key rotation, clock skew — that libraries handle and hand-written code forgets. Your job is to configure it correctly and validate what it returns.

What To Check

Before moving on, make sure you can:

  • state the OAuth-vs-OIDC distinction: authorization versus authentication
  • name the four roles and walk the authorization code flow
  • explain why the code is exchanged server-side and what PKCE adds
  • explain what state defends against and why it is mandatory
  • validate an ID token: signature, issuer, audience, expiry
  • key local accounts off sub, not email, and say why

What You Should Be Able To Do

After this lesson, you should be able to integrate an OAuth/OIDC provider correctly with a library, implement the state and token-validation guards, distinguish when you need delegated access versus authentication, and avoid the trust-the-unvalidated-token and match-on-email mistakes.

Practice

Practice: Design A "Sign In With Google" Flow
Show solution

Outbound (step 1): redirect to Google with client_id, scope=openid email profile, a freshly generated random state stored in the session, a nonce also stored, and the PKCE code_challenge.

Callback: first verify state matches the session value and reject otherwise — this is the CSRF guard and it is non-negotiable. Then exchange the code server-to-server with the client secret and PKCE verifier for tokens.

Validation of the ID token, all four checks: signature against Google's published JWKS keys, iss is Google's issuer, aud equals our client_id, exp is in the future, and nonce matches the stored value. Only a token passing all of these is trusted.

Account mapping: look up the local account by the sub claim, never by email. First-time sub means either a new signup (create an account keyed on sub) or a linking decision if an email-matching account exists — and that linking is handled carefully per the account-linking lesson, not by silently trusting the email. Email is stored as a profile attribute, not as the identity key.

Everything rides over HTTPS, and the library does the cryptographic heavy lifting while our code enforces the four validations and the state check.

Practice: Diagnose A Token Trust Bug

An app logs users in "with Google" by taking the ID token the browser posts to its callback, base64-decoding the payload, reading the email claim, and logging the user into whatever local account has that email. A researcher demonstrates logging in as any user.

Task

Explain every way this is broken and the correct validation.

Show solution
  • No signature verification. Base64-decoding a JWT reads its claims without checking they are authentic. An attacker crafts a token with any email they like — or takes a real token and edits the payload — and the app believes it. The signature is the entire security of a JWT; skipping it makes the token a plain untrusted form field.
  • No issuer check. Even a validly signed token might come from a different OIDC provider entirely; without checking iss, a token from any issuer the app never intended is accepted.
  • No audience check. A token legitimately minted for a different application (with a different aud) is accepted here — the confused-deputy attack. Any site the victim used with the same provider can yield a token this app will trust.
  • No expiry check. Old, possibly-leaked tokens work forever.
  • Matching on email instead of sub. Even with validation fixed, keying the account off a mutable email invites takeover when an email is reassigned or when the provider does not guarantee email uniqueness.

Correct validation: verify the signature against the provider's published keys, then assert iss, aud == our client_id, exp in the future, and nonce; and only then map to the local account by sub. In practice, do not receive the ID token from the browser at all — run the authorization code flow so the token is fetched server-to-server from the provider, which removes the attacker's ability to supply one.

Practice: Define OAuth/OIDC Review Checks
Show solution
  • A test drives the callback with a missing or mismatched state and asserts rejection — the CSRF guard regressing silently is a real risk.
  • Tests feed the token validator a token with a bad signature, a wrong aud, a wrong iss, and an expired exp, each asserted rejected. These four tests are the core of the suite because they encode the whole trust decision.
  • A test confirms account lookup keys on sub, and that two identities with the same email but different sub do not collapse into one account.
  • A check asserts the app never accepts an ID token from a browser-posted field — the flow fetches tokens server-side.

Human, periodically:

  • Confirm the provider's signing keys are fetched from discovery/JWKS and refreshed (key rotation handled by the library, not pinned to a stale key).
  • Re-read scopes requested against what the app uses — scope creep grants access the app does not need, widening breach impact.
  • Verify the client secret is stored as a secret (not in the repo), consistent with the secret-management lessons.