| | | 1 | | using ArturRios.Heimdall.Command.Services; |
| | | 2 | | using Google.Apis.Auth; |
| | | 3 | | |
| | | 4 | | namespace ArturRios.Heimdall.WebApi.Security; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Verifies UC-25's Google ID tokens against Google (FR-GO-11, NFR-13). |
| | | 8 | | /// <see cref="GoogleJsonWebSignature.ValidateAsync(string, GoogleJsonWebSignature.ValidationSettings)" /> |
| | | 9 | | /// checks the signature against Google's |
| | | 10 | | /// published keys, the issuer against <c>accounts.google.com</c>, and the expiry; the configured |
| | | 11 | | /// client IDs constrain the audience, so a token minted for someone else's application is |
| | | 12 | | /// refused even though Google signed it. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// Every failure becomes <c>null</c> rather than an exception, because AF-25a treats them alike |
| | | 16 | | /// and the handler has no decision to make between "expired" and "forged". The reason is logged |
| | | 17 | | /// at debug level so an operator can still tell them apart; it is deliberately not returned to |
| | | 18 | | /// the caller. |
| | | 19 | | /// </remarks> |
| | 0 | 20 | | public class GoogleIdTokenVerifier( |
| | 0 | 21 | | GoogleSignInOptions options, |
| | 0 | 22 | | ILogger<GoogleIdTokenVerifier> logger) : IGoogleIdTokenVerifier |
| | | 23 | | { |
| | | 24 | | public async Task<GoogleIdTokenPayload?> VerifyAsync(string idToken) |
| | | 25 | | { |
| | 0 | 26 | | if (string.IsNullOrWhiteSpace(idToken)) |
| | | 27 | | { |
| | 0 | 28 | | return null; |
| | | 29 | | } |
| | | 30 | | |
| | | 31 | | try |
| | | 32 | | { |
| | | 33 | | // The settings are built by GoogleIdTokenValidationSettings so the audience constraint |
| | | 34 | | // is something a test can see — see its remarks for why that one is worth isolating. |
| | 0 | 35 | | var payload = await GoogleJsonWebSignature.ValidateAsync( |
| | 0 | 36 | | idToken, GoogleIdTokenValidationSettings.For(options)); |
| | | 37 | | |
| | | 38 | | // Reached only once ValidateAsync has returned, i.e. once signature, issuer, audience |
| | | 39 | | // and expiry are all verified: the claim is never read off an untrusted token. Why the |
| | | 40 | | // claim is re-read off the token at all, rather than taken from the typed payload, is |
| | | 41 | | // GoogleIdTokenClaims.EmailVerified's business. |
| | 0 | 42 | | return new GoogleIdTokenPayload( |
| | 0 | 43 | | payload.Subject, |
| | 0 | 44 | | payload.Email, |
| | 0 | 45 | | GoogleIdTokenClaims.EmailVerified(idToken, logger), |
| | 0 | 46 | | payload.Name, |
| | 0 | 47 | | payload.Picture, |
| | 0 | 48 | | string.IsNullOrEmpty(payload.Nonce) ? null : payload.Nonce); |
| | | 49 | | } |
| | 0 | 50 | | catch (InvalidJwtException exception) |
| | | 51 | | { |
| | 0 | 52 | | logger.LogDebug(exception, "Google ID token rejected"); |
| | | 53 | | |
| | 0 | 54 | | return null; |
| | | 55 | | } |
| | 0 | 56 | | } |
| | | 57 | | |
| | | 58 | | } |