How ‘Sign in with Google’ Actually Works: A Deep Dive into Cognito + OAuth2
Today I dug into how Google Sign-In works when it’s wired up through AWS Cognito. You’ve probably seen that “Sign in with Google” button on countless apps and wondered what’s actually happening behind the scenes. Here’s how it works.
Here’s a breakdown of the flow, step by step, along with example URLs and payloads.
Why OAuth2 in the first place?
Before OAuth2, if you wanted to let users sign in with their Google account, you’d basically need Google to hand you their username and password so you could verify it, which is a terrible idea for obvious security reasons. Nobody wants to give a third-party app their Google credentials.
OAuth2 solves this by introducing a delegation model: instead of sharing credentials, the user grants your app a token that proves “yes, I am who I say I am, and I’ve allowed this app to know that.” The actual authentication (checking password, 2FA, etc.) happens entirely on Google’s turf. Your app never sees a password, it only ever sees short-lived tokens that Google (or in this case, Cognito acting as a middleman) issues after the user consents.
This makes auth simpler in a few ways:
- You don’t have to build or store passwords for your users.
- You inherit Google’s security (2FA, device checks, breach detection) for free.
- The “identity provider” (Google) and “resource” (your app) are cleanly separated, so tokens can be scoped, expired, and revoked independently.
Cognito sits in the middle of this as an identity broker, it talks to Google on your behalf, and then issues its own tokens to your app, so your app only ever has to integrate with one system (Cognito) instead of every identity provider individually.
The Flow, Step by Step
Step 1: App redirects to Cognito’s authorize endpoint
Your app kicks things off by sending the user’s browser to Cognito’s /oauth2/authorize endpoint.
https://my-app-prod.auth.us-east-1.amazoncognito.com/oauth2/authorize?
client_id=1a2b3c4d5e6f7g8h9i0j&
response_type=code&
scope=openid+email+profile&
redirect_uri=https://api.my-app.com/auth/callback&
identity_provider=GoogleHere, redirect_uri is the endpoint (in my case, a Lambda function) that Cognito will eventually send the user back to, but not yet. This value gets remembered by Cognito for later validation.
Step 2: Cognito forwards the request to Google
Cognito recognizes identity_provider=Google and redirects the browser again, this time to Google's own OAuth endpoint, constructing the required parameters itself (using the Google client ID/secret Cognito has registered with Google).
https://accounts.google.com/o/oauth2/v2/auth?
client_id=your-google-client-id.apps.googleusercontent.com&
response_type=code&
scope=openid+email+profile&
redirect_uri=https://my-app-prod.auth.us-east-1.amazoncognito.com/oauth2/idpresponse&
state=abc123xyzNotice the redirect_uri here points back to Cognito, not your app. Google doesn't know your app exists, it only knows Cognito.
Step 3: User authenticates and consents on Google
The user sees the familiar Google sign-in screen, logs in, and approves the requested scopes (email, profile, etc.).
Step 4: Google redirects back to Cognito with a code
Once the user consents, Google redirects the browser to Cognito’s /oauth2/idpresponse endpoint with an authorization code:
https://my-app-prod.auth.us-east-1.amazoncognito.com/oauth2/idpresponse?
code=4/0AX4XfWh...GoogleCode&
state=abc123xyzStep 5: Cognito exchanges the Google code, then issues its own
This is the part that’s easy to misunderstand. Cognito doesn’t just relay Google’s code onward, it does its own separate exchange:
- Cognito takes the Google code and calls Google’s token endpoint server-to-server, using its own registered Google client ID/secret, to get Google’s
id_token/access_token. - Cognito uses the Google
id_tokento create (or link) a user in your Cognito User Pool. - Cognito then generates a brand-new authorization code of its own and redirects the browser to the original
redirect_urifrom Step 1 (it knows which one because it tracked that against the session/state it created back then).
https://api.my-app.com/auth/callback?
code=cognitoAuthCode789&
state=abc123xyzSo there are actually two separate codes involved, Google’s code (‘X’ in image reference) and Cognito’s code (‘Y’ in image reference), for two separate token exchanges.
Step 6: Your Lambda exchanges Cognito’s code for tokens
Your redirect_uri (a Lambda function, in my case) receives this Cognito code and makes a server-to-server POST request to Cognito's token endpoint:
POST https://my-app-prod.auth.us-east-1.amazoncognito.com/oauth2/token
Content-Type: application/x-www-form-urlencoded
//request body
grant_type=authorization_code&
client_id=1a2b3c4d5e6f7g8h9i0j&
code=cognitoAuthCode789&
redirect_uri=https://api.my-app.com/auth/callbackA subtlety I initially got confused about: this redirect_uri isn't being used to redirect anywhere, this call is a plain POST. It's included purely as a security check: Cognito compares it against the redirect_uri used back in Step 1. If they don't match, the exchange is rejected. This stops a stolen code from being redeemed via a different endpoint than the one that started the flow.
Get Aryan Chari’s stories in your inbox
Join Medium for free to get updates from this writer.
Cognito responds with tokens:
{
"id_token": "eyJraWQiOiJ...",
"access_token": "eyJraWQiOiJ...",
"refresh_token": "eyJjdHkiOiJ...",
"expires_in": 3600,
"token_type": "Bearer"
}Step 7 (optional): Fetch user details
The id_token is a JWT that already carries basic claims; email, sub, name, etc; decoded straight from the token itself, no extra call needed. If your app wants app-specific data (profile settings, permissions, order history, whatever lives in your own database), you'd look that up here using the sub (user ID) as the key.
This step is optional, if all you need is to prove who the user is, you can skip a database round-trip entirely and just pass the tokens straight through.
Step 8: Pass tokens (and optionally user details) to the frontend
Finally, the Lambda returns the tokens, and the user details, if fetched, to the frontend, which stores them (however it manages sessions) and treats the user as logged in.
Conclusion
The biggest thing I learned is that this isn’t a single OAuth handshake; it’s two chained OAuth flows stitched together: Google↔Cognito, and Cognito↔your app. Cognito acts as a broker that abstracts away Google’s implementation details, so your app only ever needs to understand one OAuth integration (Cognito’s), no matter how many identity providers you plug in behind it. The redirect_uri also does double duty, first as a literal redirect target, and later as a security check during token exchange, which was the part that tripped me up the most until I understood why it's sent twice.





