Skip to content

OAuth and delegated identity

Every "Sign in with Google" button is a decision not to run a password system.

Your app doesn't check a password, doesn't verify the person, and never stores a credential. It asks a provider it already trusts, and takes the answer.

The protocol underneath is OAuth, and the flow has more moving parts than the button suggests.

Two different authorizations

One thing trips almost everyone up, and it's worth clearing up before the flow makes sense.

You've seen the consent screen: an app asking for your name, profile photo and email address. Sometimes more, sometimes a lot more, like contacts and calendar and every message in your inbox.

That screen is authorization, and it has nothing to do with your app's roles. It decides which parts of the user's Google account your app may reach, and those permissions are called scopes.

Your own admin and user roles are still decided inside your back end, after the login finishes, exactly as in the previous chapters.

OAuth authorizes, OIDC identifies

Strictly, OAuth grants delegated authorization: permission for your app to act on a resource. The identity layer built on top of it is OpenID Connect, or OIDC, and the "sign in with" buttons are OIDC.

The distinction matters in code. An OAuth access token says what your app may reach; an OIDC ID token says who the user is. Treating the first as proof of identity is a common and real mistake.

JunoTwo different authorizations Two things are being decided here, and they blur together constantly.

Google decides which of your Google details the app can see. Your app decides what that person can do once they're in. Different question, different place, different answer.

JunoTwo different authorizations Scopes are set when you configure the provider, not per user, so it's a decision made once and then rarely revisited. Which is how apps end up requesting far more than they use.

Worth putting on a checklist: before shipping an OAuth integration, list every scope you request and name the feature that needs it. Anything without a feature comes out.

JunoTwo different authorizations The practical version of the OIDC distinction is which token you read. The ID token is a JWT with claims about the user, meant for your app, and it's the one to verify and consume.

The access token is meant for the provider's APIs, and your app should treat it as opaque.

Reading identity out of an access token works right up until the provider changes its format, because they never promised you that shape.

It also invites the confused deputy problem: an access token issued to a different app gets presented to yours and accepted, since nothing in it says who it was for.

That's what the aud claim on the ID token exists to prevent, and checking it is not optional.

The round trip

Eight steps, and the user leaves your app in the middle of them:

  1. The user presses the button in your app.
  2. Your app redirects them to Google.
  3. The user logs in, directly with Google. Your app sees none of this.
  4. Google shows the consent screen for the scopes you asked for.
  5. If the user agrees, Google sends your server a one-time approval code. This code logs nobody in. It represents consent, nothing more.
  6. Your server sends that code straight back to Google, along with your app's own secret credentials.
  7. Those credentials prove to Google that the request came from your back end.
  8. Google replies with an ID token saying who the user is, and your app logs them in.

The two-step handover in five to eight is the part worth understanding. The code travels through the user's browser and is useless on its own; the token that actually matters is exchanged server to server, where a browser never sees it.

JunoThe round trip The redirect away and back is the whole trick. Your app is deliberately not present while the password is typed, so there's nothing for it to accidentally keep.

That's the appeal: you can't leak a credential you never received.

JunoThe round trip Steps five and six answer a question people ask early: why a code first, instead of Google sending the token straight away?

Because the code passes through the browser, where things get logged and shared. It's short-lived and worth little alone.

The exchange in step six happens server to server with your secret attached, so the valuable token never touches the browser.

JunoThe round trip Two additions the lesson doesn't reach, both now standard. PKCE, Proof Key for Code Exchange, has the client generate a secret before step two and present it at the exchange, so a stolen code cannot be redeemed by whoever took it.

It began as a mobile fix, and current guidance applies it everywhere.

And the state parameter: a random value you send in step two and check when the user returns, which ties the response to the request you started. Without it, an attacker can complete a flow of their choosing in the victim's browser and link their account to your victim's session.

Both are cheap. Both are routinely missing from hand-rolled integrations, which is the strongest argument for using a maintained library rather than writing the flow yourself.

Three ways it goes wrong

Sending the user away and trusting the provider to send them back opens three quiet gaps.

What goes wrongWhy it mattersThe fix
1Asking for too muchScopes beyond what the app uses mean a leak exposes far more than it should, and users lose trust in an app that looks nosy. Information disclosureRequest the smallest set your features actually use
2The approval code leakingThe code turns up in the URL bar, browser history, analytics or server logs. Anyone who sees it before it expires can finish the login as that user. SpoofingKeep it out of anything that gets logged, and exchange it immediately
3Trusting the ID token uncheckedAnyone can write a token that looks right. Only Google can sign one. Accepting it unverified lets an attacker claim to be anybody. SpoofingVerify the signature, the audience, and the expiry, before trusting anything in it

Pitfall 3 is the one that undoes the whole model. The point of delegating was that a trusted party vouches for the user; skipping verification means you're trusting whoever sent the request instead.

JunoThree ways it goes wrong The note comparison works here. Google hands your app a signed note saying who the user is.

Anyone can write a note. Checking the signature is how you know this one came from Google, and skipping that check is trusting handwriting you've never seen.

JunoThree ways it goes wrong Verification is three checks, not one, and the second is the one people skip. Was it signed by the provider? Was it issued for your app? Has it expired?

The middle check is the audience claim. Without it, a token minted for a different application is a valid signature from Google that says nothing about whether it was meant for you.

JunoThree ways it goes wrong Account linking is the sharp edge the pitfalls list leaves out. Matching a delegated login to an existing account by email address is what everyone reaches for, and it's only safe when the provider verifies the address. Check the email_verified claim, and treat its absence as unverified.

Get it wrong and someone registers an account at a provider using your user's email address, signs in through it, and lands inside the existing account.

The other thing to plan before launch is what happens when the provider is down or a user loses access to it. A delegated-only login makes your availability theirs, and account recovery becomes a conversation about someone else's account recovery policy.

Try it

Five situations from a live OAuth integration. Name the pitfall, the risk, and the fix.

  1. Your app needs only an email address to log people in, and the OAuth request also asks for contacts, calendar and Drive files.
  2. The consent screen warns that your app wants permission to delete calendar events. Your app never touches Calendar.
  3. After login, the page briefly shows the short-lived approval code in the URL.
  4. Your back end receives the ID token and accepts it without checking the signature or who it was issued for.
  5. Your back end accepts an ID token that expired an hour ago.
Compare your answers
#PitfallRiskFix
1Asking for too muchA leaked token exposes far more of the user's account than the app ever neededRequest only the scopes a feature uses
2Asking for too muchSame exposure, plus a consent screen that makes the app look untrustworthy before anyone has used itDrop the unused scope
3Leaked approval codeURLs get logged, saved and shared. Whoever captures the code before it expires can complete the loginKeep codes out of the URL, exchange immediately
4Trusting the ID tokenAnyone can forge something token-shaped. Without signature and audience checks, an attacker can claim to be any userVerify signature and audience before trusting it
5Trusting the ID tokenAn old token can be replayed to log in as that user repeatedlyCheck expiry and reject anything past it

Two and one are the same pitfall with different consequences, which is why the list is worth reading as three problems rather than five. Four and five are also one pitfall: verification is a set of checks, and skipping any of them is skipping verification.

Where this goes next

Three models, each solving the same problem from a different direction. The server remembers, the client carries proof, or a provider vouches.

Choosing an identity model puts all three side by side and works through when each one is the right answer.