Support

Lesson 3 of 11 ·13 min read ·Reviewed 6 August 2026

A Login Flow End to End

"Sign in with Google" from a mobile app, request by request.

1. What flow this actually is

The button says "Sign in with Google", but mobile apps reach the same result through two common client-side routes. Google's current Android integration uses Credential Manager to return an ID token directly. On iOS, and with providers that support browser-based native OAuth, the visible machinery is the OAuth 2.0 authorization code flow with PKCE, carrying OpenID Connect (OIDC) on top. Both routes meet your backend at the same verification seam. The OAuth/OIDC distinction is worth keeping straight:

  • OAuth 2.0 answers "what is this app allowed to do?" (authorization). It delivers an access token.
  • OpenID Connect is a thin layer on OAuth that answers "who is this user?" (authentication). It delivers an ID token, a signed JWT stating the user's identity.

Many "Sign in with X" buttons run some version of this authorization-code dance, but the artefacts vary: OIDC providers return an ID token, while OAuth-only providers require another request to learn who signed in. Learn the seam between provider identity and your own API session and the variations become much easier to read. If you have worked through the free guide OAuth 2.0 in Python: The Authorization Code Flow Explained, this is the same family of flow seen from the app's side of the wire.

2. The browser-based sequence at a glance

Google on Android takes a shorter client-side route. Credential Manager presents the account sheet and returns a Google ID token; your app supplies the backend-issued nonce through setNonce(), then rejoins this diagram at step 6. The app does not directly open Google's authorization endpoint, receive a redirect, or exchange an authorization code. If the app also needs access to Google data such as Drive files, Android treats that as a separate authorization action.

3. Before any user shows up: registration

The developer registers the app in the Google Cloud console and receives a client ID. The browser-based route also needs a registered redirect URI. Google still supports a reversed-client-ID custom scheme on iOS; other providers may support custom schemes or claimed https links that the operating system verifies belong to the genuine app.

Google-specific Android rule: Google no longer accepts custom-scheme redirects for Android because another app can impersonate the scheme owner. Use Credential Manager for Google sign-in on Android. The custom-scheme examples below therefore show the Google-on-iOS/browser branch, not an Android implementation.

No client secret. A confidential web server can authenticate with a client secret; a mobile app deliberately cannot. A shared value compiled into an app installed on a million phones is extractable, so the authorization server treats the app as a public client. PKCE does not make the app confidential or prove it is genuine; it protects the authorization code so an interceptor cannot redeem it without the one-time verifier.

4. The flow, step by step

App ↔ Your backend

Step 1: The tap. Create the transaction

For the browser-based branch, the moment the user taps the button, the app generates three throwaway values:

  • code_verifier: a long random string, kept in memory, never sent yet.
  • code_challenge: the SHA-256 hash of the verifier. This one gets sent.
  • state: a random value to bind the request to the response (CSRF protection).

Both branches ask your backend for a short-lived, one-time nonce. The backend records that pending login challenge and returns the random value to the app. The browser branch includes it in the authorization request; Credential Manager receives it through setNonce(). Google must return the same value inside the ID token. When the token reaches your backend in step 6, the backend can match the claim to state it already trusts and consume the challenge exactly once.

iOS / compatible provider → Browser

Step 2: Open the system browser at Google's authorization endpoint

In this branch, the app opens an external system-browser surface such as ASWebAuthenticationSession on iOS, crucially not an embedded WebView. The request is a plain GET:

Authorization request
https://accounts.google.com/o/oauth2/v2/auth
  ?client_id=1234-abc.apps.googleusercontent.com
  &redirect_uri=com.googleusercontent.apps.1234-abc%3A%2Foauth2redirect
  &response_type=code
  &scope=openid%20email%20profile
  &state=xyz123
  &code_challenge=E9Mel...
  &code_challenge_method=S256
  &nonce=n0nc3

scope=openid email profile is what makes this an OIDC request: openid asks for an ID token; email and profile request the user's email address and basic profile, which Google delivers as claims inside that token.

Why never a WebView: a WebView is rendered inside the app, so the app could read every keystroke, including the user's Google password. The system browser is a separate OS surface the app cannot see into. It also shares cookies with the user's real browser, so an already-signed-in user skips the password entirely. Google actively detects and blocks WebView logins for exactly this reason.

User ↔ Google

Step 3: The user authenticates on Google's turf

Password, passkey, two-factor prompt, or nothing at all if a session cookie already exists. If it's the app's first time, Google shows a consent screen listing the requested scopes. The app never sees any of this exchange; that is the entire point of the design.

Google → Browser → App

Step 4: The redirect hands back an authorization code

Callback redirect (sample)
com.googleusercontent.apps.1234-abc:/oauth2redirect?code=4/0AX4Xf...&state=xyz123

The OS sees the custom scheme (or App Link) and routes control back to the app. The app immediately checks the returned state equals the one from step 1; a mismatch means someone is injecting a foreign response, so the app aborts.

Note what the code is: a provider-defined, short-lived, single-use voucher. It is not a token and grants nothing by itself.

App → Google

Step 5: Exchange the code for tokens (the PKCE moment)

Token exchange request
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

code=4/0AX4Xf...
&client_id=1234-abc.apps.googleusercontent.com
&redirect_uri=com.googleusercontent.apps.1234-abc:/oauth2redirect
&grant_type=authorization_code
&code_verifier=dBjftJeZ4CVP...   <- the one-time verifier from step 1

Google hashes the code_verifier and compares it with the code_challenge from step 2. Match → tokens. This is PKCE's trick: even if a malicious app on the same phone intercepted the redirect and stole the code, it never had the verifier, so the code is useless to it.

The response:

Token response (sample)
{
  "access_token": "ya29.a0Af...",     // for calling Google's own APIs
  "id_token":     "eyJhbGciOi...",    // signed JWT: who the user is
  "refresh_token": "1//0gFt...",      // returned for installed apps; unused for login alone
  "expires_in": 3599
}
App → Your backend

Step 6: Your backend verifies the ID token

The step most tutorials skip. Google has authenticated the user, but your API has not. The app sends the ID token to your backend:

Backend login request (sample)
POST /auth/google
{ "id_token": "eyJhbGciOi..." }

The backend must verify it properly, not just decode it:

  • Signature: checked against Google's published public keys (the JWKS endpoint, cached and rotated).
  • iss: is one of Google's documented issuer values: https://accounts.google.com or accounts.google.com.
  • aud: is one of the client IDs your backend accepts (a valid Google token minted for someone else's app must be rejected).
  • exp: not expired.
  • nonce: matches an unexpired login challenge the backend issued in step 1. The backend consumes it atomically so the same ID token cannot establish a second session.

A nonce merely supplied beside the token would prove nothing: an attacker replaying a captured token could send its nonce too. In this design the check has teeth because the backend recognises the value from its own pending-login store.

The classic vulnerability: decoding the JWT payload and trusting it without verifying it. A decoded payload is only base64url-encoded data; anyone can fabricate one claiming to be anyone. Use a maintained library rather than hand-rolled verification. In Python, google-auth verifies the signature and standard claims such as issuer, audience, and expiry. Your application must still compare the nonce with the pending challenge stored by your backend and consume it once.

Your backend → App

Step 7: Your backend issues its own tokens

The backend looks up or creates the user keyed on the token's sub claim (Google's permanent, unique user ID). Key on sub, not email: emails can change and, on some providers, be unverified. Then it mints its own access token + refresh token for your API and returns them. From this point Google is out of the picture; the session is purely between your app and your backend.

App

Step 8: Steady state

  • Tokens stored in the Keychain on iOS; on Android, encrypted storage whose encryption keys are protected by the Keystore. Never plain files or preferences.
  • Every API call: Authorization: Bearer <access token>.
  • Access token expires (typically 15–60 min) → the app silently POSTs the refresh token to /auth/refresh for a new pair.
  • Rotation with reuse detection: each refresh issues a new refresh token and invalidates the old one. The backend retains token-family state; if an invalidated ancestor appears again, that reuse reveals likely theft and the backend revokes the family.
  • Logout: delete local tokens and call the backend to revoke the refresh token server-side. Deleting locally alone leaves a live credential in the wild.

Lesson 10 returns to secure storage and the limits of hardening a public client.

5. What each artefact is for

ArtefactIssued byLifetimePurpose
code_verifier / code_challengeThe appOne flowProves the token exchange comes from the same flow instance that started it (PKCE)
stateThe appOne flowBinds the redirect to the request; blocks injected responses (CSRF)
nonceYour backendOne flowBinds the ID token to a pending login transaction; consumed once to block replay
Authorization codeGoogleShort-lived, single useVoucher exchanged for tokens; an interceptor cannot redeem it without the PKCE verifier
Google access tokenGoogle~1 hourCalling Google's own APIs on the user's behalf
ID tokenGoogle~1 hourSigned proof of identity; accepted for login only with an unused backend-issued nonce
Your access tokenYour backend15–60 minAuthenticates every call to your API
Your refresh tokenYour backendWeeks/months, rotatedSilently renews the access token; revocable server-side

6. Why each defence exists

AttackDefence
Malicious app registers the same custom scheme and catches the redirectPKCE makes the stolen code useless without the verifier; claimed https links bind the URI to the genuine app. Google goes further on Android and does not support custom-scheme redirects
Attacker tricks the app into completing their login (a login CSRF: the victim ends up signed into the attacker's account)state check on the redirect
Replay of a captured ID tokenBackend-issued, atomically consumed nonce; short exp
Forged JWT claiming to be any userSignature verification against Google's JWKS
Valid Google token from a different app replayed against your APIaud must match one of your backend's accepted client IDs
App harvesting the user's Google passwordSystem browser, never a WebView; Google blocks WebView logins
Stolen long-lived refresh tokenSecure storage + rotation with token-family reuse detection

7. Variations worth knowing

  • Web instead of mobile: same flow, but the redirect URI is an https URL on your domain, the exchange in step 5 happens server-side with a client secret, and the session usually ends up in an httpOnly cookie rather than stored tokens. PKCE is now recommended there too. This server-side variant is exactly what the OAuth guide builds in Python.
  • Google on Android: Credential Manager presents a native account sheet and returns a Google ID token for your backend to verify. It handles authentication; requesting access to Google data is a separate operation through Google's authorization API.
  • Other native sign-in SDKs: Sign in with Apple and provider SDKs may present native UI instead of a browser tab. The returned artefacts vary, but your backend still verifies provider-issued evidence before creating its own session.
  • "Sign in with GitHub/Spotify/etc.": many use an authorization-code dance, but some providers are OAuth-only and return no OIDC ID token. In that case the backend calls the provider's /user endpoint with the provider access token to learn who logged in, then continues from step 7.
  • Magic links (the model endpoint51.com itself uses): a different first factor entirely (possession of an inbox instead of a Google account), but it converges at the same place. The backend confirms identity, then issues its own session credential. Steps 7–8 are universal; only steps 1–6 vary by provider.

The takeaway shape: apart from the one-line backend nonce request, steps 1–5 are the provider-facing machinery; Android Credential Manager compresses those client-side steps into its own API. Steps 6–8 are your API's session machinery. A "social login" is two auth systems stitched together at that seam, and once you see it, unfamiliar login flows become much easier to decompose.

Check your understanding

  • Why does a mobile app get no client secret, and what specific interception attack does PKCE prevent?
  • Why does this design have the backend issue and remember the OIDC nonce instead of accepting a nonce supplied alongside the ID token?
  • Which check stops a valid Google token minted for a different app being accepted by your API?
  • At which step does Google leave the picture, and what does your backend issue in its place?

Reviewed 6 August 2026.