Verifying a Google token yourself
When a mobile or SPA client sends you an ID token, what you must check.
Open this lesson in the learning hubKey points
- A common architecture has the client complete the Google flow and send the resulting ID token to your backend. Your backend must then verify it properly - the client cannot be trusted to have done so.
- Verify the signature against Google published keys, and cache them. Google rotates regularly, so a hard-coded key is an outage on a schedule.
- Check
audequals your client id. Without it, a token minted for any other application is accepted, which is the confused-deputy attack. - Check
issisaccounts.google.comorhttps://accounts.google.com- Google uses both forms, and accepting only one causes intermittent failures. - Check
email_verifiedbefore trusting the email for anything, and still resolve the account bysubrather than by email. - Never accept an access token as proof of identity. It is a bearer credential for Google APIs, carries no audience binding to you, and is exactly what the token-swap attack uses.
Example
// The client sends the ID TOKEN, never the access token.
@PostMapping("/auth/google")
public ResponseEntity<SessionResponse> signIn(@RequestBody TokenRequest request) {
GoogleIdToken idToken;
try {
// The verifier caches Google keys and handles rotation.
idToken = verifier.verify(request.idToken());
} catch (GeneralSecurityException | IOException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
if (idToken == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
GoogleIdToken.Payload payload = idToken.getPayload();
// Signature, aud, iss and exp are checked by the verifier below.
// These two are YOUR responsibility:
if (!Boolean.TRUE.equals(payload.getEmailVerified())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// Resolve by sub - the stable id - never by email.
Account account = accounts.findByGoogleSub(payload.getSubject())
.orElseGet(() -> accounts.createFrom(payload));
// Issue YOUR session or token. The Google token stops here.
return ResponseEntity.ok(sessions.start(account));
}
@Bean
GoogleIdTokenVerifier verifier(@Value("${google.client-id}") String clientId) {
return new GoogleIdTokenVerifier.Builder(
new NetHttpTransport(), GsonFactory.getDefaultInstance())
// aud MUST be your client id - this is the confused-deputy check
.setAudience(List.of(clientId))
// Google issues BOTH forms; accept both or you get flaky 401s
.setIssuers(List.of("accounts.google.com", "https://accounts.google.com"))
.build();
}
/*
* THE ATTACK IF YOU ACCEPT AN ACCESS TOKEN INSTEAD:
*
* 1. attacker builds any app; a victim grants it access
* 2. attacker now holds a valid Google ACCESS token for that victim
* 3. attacker posts it to YOUR /auth/google
* 4. you call Google userinfo with it, get the victim details,
* and sign the attacker in as the victim
*
* An access token has no audience binding to you. Only the ID TOKEN,
* with aud == your client id, proves the user signed in TO YOUR APP.
*/
Verify the ID token with aud set to your client id - an access token proves nothing about which app the user signed in to.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OAuth with Google course, and every lesson in it is listed on the OAuth with Google contents page.