What Actually Happens When You "Log In With Google"

You click a button. The page flickers through a couple of redirects. You're logged in.

You click a button. The page flickers through a couple of redirects. You're logged in.

Somewhere in those two seconds, four parties exchanged messages, several cookies were set by different domains, a cryptographic proof was constructed and verified, and your browser carried a signed statement about your identity between two systems that have never directly communicated.

Most explanations of this either stay at the level of "OAuth handles it" or dive straight into the RFC. What follows is the actual sequence — every redirect, every parameter, and specifically who sets which cookie, because that's the part almost every explanation skips and it's where the confusion lives.

The cast

You, in a browser. Note that the browser is the only participant present at every step. It's the transport.

The application you're logging into. In OAuth terms, the client. It has a client ID (public) and a client secret (kept on its server, never in the browser).

Google, acting as the identity provider. Runs the authorization endpoint your browser visits and the token endpoint that only servers talk to.

The key structural fact: the application and Google never talk directly until the very last step. Everything before that is your browser carrying messages between them.

Step 1: You click the button

The application's server builds a URL and redirects you to it. The parameters matter, so here's a realistic one, wrapped for readability:

https://accounts.google.com/o/oauth2/v2/auth
  ?client_id=1234.apps.googleusercontent.com
  &redirect_uri=https://app.example.com/auth/callback
  &response_type=code
  &scope=openid%20email%20profile
  &state=xY3kf9Qm2Lp
  &nonce=8vNq1Zr7Tc
  &code_challenge=E9Melhoa2Ow...
  &code_challenge_method=S256

Before redirecting, the application does two things you can't see:

It generates state — a random value — and stores it in a short-lived cookie on its own domain. This is a CSRF defense: when the response comes back, the app compares the returned state against what it stored. If they don't match, someone else initiated this flow and the response gets discarded.

It generates nonce and a PKCE code verifier. The nonce goes in the session too, to be checked against the ID token later. The code verifier is kept server-side; only its SHA-256 hash (code_challenge) goes in the URL. Nobody watching this redirect can derive the verifier from the challenge.

response_type=code is what makes this the Authorization Code flow — the response will contain a short-lived code, not a token. scope=openid ... is what makes this OIDC rather than plain OAuth; without openid, you'd get API access and no identity.

Step 2: You authenticate at Google

Your browser lands on Google's domain. Everything that happens here involves cookies on accounts.google.com — cookies the application cannot read, has never seen, and has no access to.

If you already have a valid Google session cookie, Google recognizes you and may not prompt at all. This is why "Log in with Google" sometimes feels instant: you didn't skip authentication, you'd already done it, possibly days ago, and Google's session cookie is still valid.

If you're not signed in, you see the Google login page, enter credentials, and possibly complete 2FA. Google sets its own session cookie.

Then, if this application hasn't been granted these scopes before, you see the consent screen: "app.example.com wants to access your email address and basic profile." Approve, and Google records that grant so you won't be asked again.

The application never sees any of this. Not your password, not your 2FA code, not Google's cookies. That separation is the entire point of the architecture — the application's servers are never in a position to learn your Google credentials, even if they wanted to.

Step 3: The redirect back

Google redirects your browser to the redirect_uri:

https://app.example.com/auth/callback
  ?code=4/0AeaYSHA...
  &state=xY3kf9Qm2Lp

Two things, and both are important for what they aren't.

The code is a short-lived, single-use authorization code — typically valid for under a minute. It's not a token. Presenting it to an API gets you nothing. On its own, it's nearly useless, which is deliberate: it travels through the browser, in a URL, where it might land in browser history, a Referer header, or a proxy log.

The state is your value coming back. The application compares it to the cookie it set in step 1. Mismatch means reject.

Google checked something before issuing this redirect: that redirect_uri exactly matches one pre-registered for this client ID. Exact string match — not a prefix, not a pattern. This is what stops an attacker from starting a flow with redirect_uri=https://evil.example/steal and harvesting codes.

Step 4: The back-channel exchange

Now the application's server talks to Google directly, for the first time. No browser involved.

POST https://oauth2.googleapis.com/token

grant_type=authorization_code
code=4/0AeaYSHA...
redirect_uri=https://app.example.com/auth/callback
client_id=1234.apps.googleusercontent.com
client_secret=<server-side secret>
code_verifier=<the original PKCE verifier>

Google verifies: the code is valid, unused, and unexpired; the client credentials are correct; the redirect_uri matches the one from the original request; and — the PKCE check — that SHA-256 of code_verifier equals the code_challenge sent in step 1.

That last check is what makes an intercepted code useless. An attacker who somehow captured the code from the browser redirect cannot exchange it, because they don't have the verifier, which never left the application's server.

The response:

{
  "access_token": "ya29.a0Af...",
  "expires_in": 3599,
  "refresh_token": "1//0gLg...",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "token_type": "Bearer"
}

Step 5: Validating the ID token

The ID token is the one that matters for login. It's a JWT with three base64url-encoded parts separated by dots: header, payload, signature. Decoded, the payload looks roughly like:

{
  "iss": "https://accounts.google.com",
  "aud": "1234.apps.googleusercontent.com",
  "sub": "110169484474386276334",
  "email": "[email protected]",
  "email_verified": true,
  "nonce": "8vNq1Zr7Tc",
  "iat": 1735689600,
  "exp": 1735693200,
  "auth_time": 1735689540
}

The application must now validate it — and must is the right word, because each check closes a specific attack:

Signature. Fetch Google's public keys from its JWKS endpoint (cached, keyed by the kid in the token header) and verify. Without this, anyone can forge a token.

iss matches Google's issuer exactly.

aud equals this application's own client ID. This is the check that stops a token issued for a different application being replayed into this one.

exp / iat are within tolerance, allowing a few seconds of clock skew.

nonce matches the value stored in session at step 1. This binds the token to this specific login attempt — a token captured from another flow can't be injected into yours.

Only after all five does the application know: Google asserts that the user identified by sub authenticated at auth_time, and this assertion was issued specifically to us, in response to the request we started.

Note sub, not email. The subject identifier is the stable key. Email addresses change, and matching users by email is how account-takeover bugs get introduced when someone's address is reassigned.

Step 6: Your actual session begins

Here's the step people forget exists, and it causes an outsized share of confusion later.

The application now creates its own session — typically a session record on its side and a session cookie on app.example.com. Google's tokens have done their job. The ID token is usually not stored; it was a one-time assertion, already verified.

You now have at least two independent sessions:

flowchart TD
    B["Your browser"] --> GC["Cookie on accounts.google.com\n(Google's session)"]
    B --> AC["Cookie on app.example.com\n(the app's session)"]
    GC -.->|"unrelated lifetimes,\nunrelated invalidation"| AC

They have different lifetimes and different invalidation rules. Logging out of the application deletes its cookie and does nothing to Google's — which is why clicking "log in with Google" again immediately signs you back in with no prompt. Nothing is broken; you never logged out of Google, only out of the app.

The whole thing

sequenceDiagram
    participant B as Browser
    participant A as app.example.com
    participant G as Google
    B->>A: Click "Log in with Google"
    A->>B: 302 → Google (state, nonce, code_challenge)
    B->>G: Authorization request
    G->>B: Login + consent (Google's cookies)
    G->>B: 302 → app callback (code, state)
    B->>A: Deliver code
    A->>A: Verify state
    A->>G: POST /token (code, secret, code_verifier)
    G->>A: access_token, id_token, refresh_token
    A->>A: Validate signature, iss, aud, exp, nonce
    A->>B: Set app session cookie → logged in

Six steps, three redirects through the browser, one direct server-to-server call, four cryptographic checks, and two independent sessions at the end.

What each piece was protecting

Worth restating compactly, because the parameters look like bureaucracy until you know what each one is for:

Element Without it
state An attacker can CSRF the login flow and get you logged into their account
nonce An ID token from another flow can be replayed into yours
PKCE An intercepted authorization code can be redeemed by whoever caught it
Exact redirect_uri matching Codes can be redirected to an attacker's server
aud validation A token issued to a different app is accepted as proof of identity here
Signature validation Tokens can simply be forged

None of it is ceremony. Each one exists because the version without it was deployed, attacked, and written up.

The genuinely elegant part is the constraint the whole design satisfies: the application learns, with cryptographic confidence, who you are — while never being in a position to learn your password, and Google never needing to know anything about the application beyond a pre-registered ID and callback URL. Two systems that don't trust each other, brokered entirely by a browser that trusts neither.