Topic 27

OAuth 2.0 and OIDC as a Client

Identity

"Sign in with Google" means Stagedoor never sees the buyer's password. The service sends her to the identity provider, the provider authenticates her by whatever means it has, and sends back a one-time code; Stagedoor exchanges the code, server to server, for tokens, one of which is a signed statement of who the provider vouches for. OAuth 2.0 is the delegation protocol underneath, OpenID Connect is the identity layer on top of it, and Stagedoor is the client in both, which is the only role this topic covers. Being the provider, the thing that issues tokens to other people's apps, is a different book.

The first implementation had the steps right and the trust wrong. It exchanged the code correctly, then read the email claim from the id token and looked up the buyer by email, without checking who had issued the token or for whom. A token minted by any provider, for any app, carrying email: marek@stagedoor.example, logged in as Marek. The flow to know is the authorization-code flow with PKCE; the mistakes are all in what the service believes at the end of it.

The Roles

Four parties. The buyer is the resource owner: the account is hers. Her browser is the user agent, the thing that gets redirected. Stagedoor is the client, registered with the provider under a client_id and, because it has a backend that can keep one, a client_secret. Google is the authorization server, and under OpenID Connect also the identity provider. OAuth alone lets Stagedoor obtain an access token for something of the buyer's, her calendar or her contacts, with her consent; it says nothing about who she is. OIDC adds the id token, a JWT of Topic 26 signed by the provider, whose claims identify her. "Sign in with" is OIDC: Stagedoor asks for scope=openid email and wants the id token, and the access token Google also returns is for calling Google's APIs, which Stagedoor does not do.

The Authorization-Code Flow

Stagedoor generates two random values and stores both in the buyer's pre-login session: a state of 128 bits, and a code_verifier of 43 to 128 characters whose SHA-256, base64url-encoded, is the code_challenge. It redirects the browser to the provider's authorization endpoint with the client_id, the registered redirect_uri, the scope, the state and the challenge. The buyer authenticates with Google and consents. Google redirects her browser back to the redirect_uri with a code and the same state. Stagedoor's backend then calls the token endpoint directly, sending the code, the client_secret and the code_verifier, and receives the id token and the access token in the response body.

The two requests that matter: the redirect the browser makes, and the exchange the backend makes
# 1. the browser is sent here; the buyer authenticates with Google, not with Stagedoor
GET https://accounts.google.com/o/oauth2/v2/auth
    ?response_type=code
    &client_id=8123...apps.googleusercontent.com
    &redirect_uri=https://stagedoor.example/auth/google/callback
    &scope=openid%20email
    &state=Q3x9...                       # random, stored in the pre-login session
    &code_challenge=E9Melhoa...            # SHA-256 of the verifier, base64url
    &code_challenge_method=S256

# 2. Google sends the browser back with ?code=4/0Ab...&state=Q3x9...; the backend exchanges it, server to server
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=4/0Ab...
&redirect_uri=https://stagedoor.example/auth/google/callback
&client_id=8123...&client_secret=...&code_verifier=dBjftJeZ...   # proves this client started the flow

# response: {"id_token": "eyJ...", "access_token": "ya29...", "expires_in": 3599, ...}

The first request is a redirect, so the buyer's browser carries it and the buyer sees Google's login page, not Stagedoor's. The second request is made by api-01 to Google over its own TLS connection, with the client secret and the verifier that only the backend knows, and the tokens come back in that response, to the backend. The browser never holds a token. It holds a code that is single-use, expires in minutes, and is worthless without the verifier and the secret, which is the whole reason the code exists as a separate step: the thing that travels through the browser is not the credential.

state and PKCE

The two random values defend against two different attacks. state binds the callback to the session that started the flow. Without it, an attacker starts a login on his own account, stops at the callback, and gets the victim to open that callback URL; the victim's browser completes the attacker's login, the victim is now signed in to Stagedoor as the attacker, and her next order lands in his order history with her card behind it. With state, Stagedoor compares the callback's value with the one in the victim's own session, finds no match, and refuses.

PKCE binds the code to the client that requested it. An attacker who intercepts the code, from a log, a referrer header or a compromised redirect, cannot exchange it, because the token endpoint demands the code_verifier whose hash was sent at the start, and the verifier never left Stagedoor's backend. PKCE was designed for mobile apps that cannot hold a secret; the OAuth 2.1 draft, still a draft as this chapter is written, makes it mandatory for every client using the authorization-code flow, and lets it stand in for state's anti-forgery role. Stagedoor sends both regardless, because state also carries where the buyer was going before login, and because two independent bindings cost 256 random bits and nothing else. OIDC adds a third value, a nonce sent on the redirect and echoed inside the id token, which binds the token itself to the session; Stagedoor sends it too.

The authorization-code flow: the browser carries a code, the backend holds the tokens
Redirectstate + challenge
Authenticateat the provider
Callbackcode + state
Exchangeserver to server
Verify id tokenas any JWT, plus aud and iss
Link by iss + subfind or create
Own sessionTopic 25

What the Id Token Proves

The id token is a JWT signed with Google's private key, and Stagedoor verifies it with Google's public keys, fetched from the JWKS URL in Google's discovery document and cached for an hour as Topic 26 described. All five checks apply, with two of them fixed: iss must be Google's issuer string, and aud must be Stagedoor's own client_id. A token issued to another app has a valid Google signature and fails the audience check, and that is the check the first implementation skipped. The nonce claim must match the one Stagedoor sent.

What the token then proves is that Google vouches for the subject named in sub, a string that is stable for that Google account for as long as it exists. That is the identity. The email claim is a fact about the account that may change, may be unverified (Google sets email_verified beside it, and not every provider does), and with Apple's relay is a forwarding address the buyer can turn off. Stagedoor links accounts by the pair of issuer and subject: a row in an identities table with iss, sub and user_id. Email is what the account page displays, and a match on email alone is never a login.

After the Flow

The id token is consumed once, at the callback, to find or create the user. Then Stagedoor issues its own session, exactly the cookie of Topic 25, as if the buyer had typed a password; the domain never learns which login path produced the principal. Google's access token is discarded unless a scope Stagedoor actually uses needs it, and Stagedoor uses none. The refresh token Google may return is not requested.

The design this replaces forwarded Google's access token to the browser as Stagedoor's credential and verified it against Google on each request. That made Google's expiry Stagedoor's expiry: an hour, then a silent logout. It made Google's revocation Stagedoor's: a buyer who removed the app from her Google account was logged out of Stagedoor with no line in any log explaining why, and support spent a week on it. And it made Google's availability Stagedoor's, one outbound call per request to a service that owed Stagedoor nothing. The provider vouches at the door; after that, the session is Stagedoor's own.

Being a Provider Is a Different Book

Everything above is the client side. The other side, issuing tokens to third-party apps, running consent screens, defining scopes as a product and rotating the keys everyone else verifies against, is a product in itself, and Stagedoor does not build one: when an organizer's box-office software wants to read ticket sales, it gets an API key of Topic 28, not an OAuth authorization. No course in the catalogue covers the provider side yet; CyberSecurity Deep Dive covers the threat model that both sides share. The comparable tools row names Keycloak, Auth0 and Ory as the way to be a provider without writing one.

Common Mistakes
  • Linking accounts by email — a provider that does not verify email, or a buyer who changes hers, and one buyer becomes another; the identity is the issuer and the subject, never the address.
  • Skipping state — a callback crafted by an attacker logs the buyer into the attacker's account, and her next order, paid with her card, appears in his order history.
  • The implicit flow — tokens delivered in the URL fragment sit in the browser history, the referrer header and every analytics script; removed in the OAuth 2.1 draft and never used here.
  • Using the provider's access token as the app's session — Google's one-hour expiry and Google's revocation become Stagedoor's, and the buyer is logged out with no log line that explains it.
  • Trusting the id token without checking aud — a token issued for any other app carries a valid Google signature and is accepted as a login to Stagedoor.
  • A redirect_uri registered with a wildcard — the provider sends the code to whichever host the attacker chose under it, and PKCE is the only thing left standing.
Best Practices
  • Use the authorization-code flow with PKCE, state and a nonce, exchange the code from the backend, and register the exact redirect_uri with no wildcard.
  • Verify the id token as any JWT, then require aud equal to your client_id, iss equal to the provider's issuer string, and nonce equal to the one you sent.
  • Link accounts by iss plus sub in an identities table; treat email as a display claim unless email_verified is true and the policy explicitly allows matching on it.
  • Issue your own session after the callback and discard the provider's tokens unless a scope you actually call requires them.
  • Fetch the provider's endpoints and JWKS from its discovery document and cache the keys for an hour, refetching once on an unknown kid.
Comparable toolsAuthlib (Python), passport (Node), Spring Security OAuth2 Client, Auth.js the client libraries that do the flow aboveGoogle, Apple, GitHub and Microsoft Entra the providers a consumer app signs in withKeycloak, Auth0 and Ory the provider side, bought or hosted rather than writtenRFC 6749, RFC 7636 and the OAuth 2.1 draft the protocol, PKCE, and the consolidation that makes PKCE mandatory

Knowledge Check

Stagedoor requests scope=openid email from Google and receives both an id token and an access token. What is each one for?

  • The access token identifies the buyer; the id token is what the browser presents on later requests
  • The id token becomes Stagedoor's session credential; the access token is stored for later refreshes
  • Both are returned to the browser, and the browser chooses which to send depending on the route
  • The id token says who Google vouches for; the access token would call Google's APIs and is discarded

An attacker starts a Google login on his own account, captures the callback URL, and tricks a buyer into opening it. Which value stops this, and how?

  • The code verifier, because the buyer's browser does not hold the verifier that the attacker generated
  • The id token signature, because the token was issued for the attacker and fails verification
  • The state value, because the buyer's session holds no match for the one in the attacker's callback
  • The redirect URI, because the provider refuses to send the code to a URL the attacker chose

Why does Stagedoor link a Google login to a user row by issuer plus subject rather than by the email in the id token?

  • Because the email claim is encrypted in the id token and cannot be read by the client
  • Because an email can change hands or be unverified, while the subject is stable for the account
  • Because Google never includes an email claim, only a subject, so there is nothing else to match on
  • Because the issuer and subject pair is shorter and indexes faster than an email address column would

After a successful callback, what does Stagedoor hand the browser?

  • Its own session cookie, the same one a password login would set
  • The id token, so that later requests can be verified against Google's keys
  • Google's access token, so that each request can be validated with Google
  • Google's refresh token, so the session can be renewed without a redirect

You got correct