Filled-in flow (server exchange with PKCE)
-
1) App → Provider: Authorization request (in browser)
-
response_type=code
-
client_id
-
redirect_uri
-
scope=openid email profile
-
state
-
code_challenge
-
code_challenge_method=S256
-
nonce (recommended)
-
2) Provider → App: Redirect back to your redirect_uri
-
code
-
state
-
error/error_description (if failed)
-
3) App → Backend: Exchange request (your endpoint, e.g. /auth/google-token-exchange)
-
code
-
codeVerifier
-
clientId
-
redirectUri
-
provider (“GOOGLE” ”APPLE” “FACEBOOK”) -
nonce (if you used one, so BE can validate it)
-
Do NOT send subId; BE will get it from the verified id_token.
-
4) Backend → Provider (token endpoint)
-
grant_type=authorization_code
-
code
-
client_id
-
redirect_uri
-
code_verifier
-
5) Provider → Backend: Token response
-
id_token (JWT with claims: sub, iss, aud, exp, nonce, email, etc.)
-
access_token
-
token_type
-
expires_in
-
refresh_token (sometimes, if offline access requested)
-
Backend actions after step 5
-
Verify id_token signature via JWKS and validate iss/aud/exp[/nonce].
-
Extract sub (and email).
-
Find/create user by (subId, provider).
- Issue your own JWTs (access/refresh) and return to the app.
Notes:
-
The app never sends subId; it’s derived server-side from the verified id_token.
-
PKCE prevents a stolen authorization code from being redeemed without the one-time code_verifier.
-
JWT secure storage protects tokens after issuance; PKCE protects the exchange before issuance.
Summary
-
Provider → App returns code and state, not subId.
-
App → Backend sends code, codeVerifier, clientId, redirectUri (and provider/nonce).
-
Provider → Backend returns id_token/access_token; BE verifies id_token, extracts sub, then mints your JWTs.