← Dev log

security

Verifying a Google ID token server-side

Verifying a Google ID token server-side

I don't run the OAuth redirect dance server-side — that happens client-side with Google's own SDK. The backend's job is verifying the ID token the frontend sends up after sign-in.

const client = new OAuth2Client(GOOGLE_CLIENT_ID);
const ticket = await client.verifyIdToken({
  idToken,
  audience: GOOGLE_CLIENT_ID,
});
const payload = ticket.getPayload(); // { email, email_verified, name, sub, ... }

The backend trusts payload.sub and payload.email to find or create the local user, then issues its own session JWT.

Never trust anything the frontend claims about identity without independent verification. A client could be modified to send a fake payload for any address. verifyIdToken cryptographically checks the token was actually signed by Google for this exact audience and hasn't expired. Skip that and trust a client-sent "I logged in as [email protected]" claim — that's a full auth bypass.

The audience parameter matters more than it looks. It must match the app's registered client ID exactly. Without it, a valid token issued for a different app could be replayed against this backend — Google signs the token, but the audience claim is what scopes it to a specific app. Easy to skip, real vulnerability if skipped.

Decide upfront how Google sign-in interacts with an existing password account under the same email — auto-link by verified email only when email_verified: true (Google can issue tokens for unverified emails in some flows), or force an explicit link step. Auto-linking on an unverified email is a real account-takeover vector.

One operational footnote: client IDs and redirect URIs are environment-specific. A recurring "works locally, breaks in Docker or prod" is the correct GOOGLE_CLIENT_ID not actually reaching the container, rather than being baked into a committed default. And Google rejects LAN-IP redirect URIs outright — anything driven from a physical device on the local network fails at the provider, not in your code, and it looks like an integration bug every single time.