using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; namespace JobTrackerApi.Services; public sealed record GoogleTokenPrincipal(string Subject, string? Email, bool EmailVerified, string? GivenName, string? FamilyName, string? Name); public interface IGoogleTokenValidator { Task ValidateAsync(string idToken, CancellationToken cancellationToken = default); } public sealed class GoogleTokenValidator : IGoogleTokenValidator { private readonly IConfiguration _cfg; private readonly IConfigurationManager _configManager; public GoogleTokenValidator(IConfiguration cfg) { _cfg = cfg; _configManager = new ConfigurationManager( "https://accounts.google.com/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever()); } public GoogleTokenValidator(IConfiguration cfg, IConfigurationManager configManager) { _cfg = cfg; _configManager = configManager; } public async Task ValidateAsync(string idToken, CancellationToken cancellationToken = default) { var audience = (_cfg["Auth:GoogleClientId"] ?? "").Trim(); if (string.IsNullOrWhiteSpace(audience)) { throw new InvalidOperationException("Google sign-in is not configured."); } var config = await _configManager.GetConfigurationAsync(cancellationToken); var handler = new JwtSecurityTokenHandler(); var principal = handler.ValidateToken(idToken, new TokenValidationParameters { ValidateIssuer = true, ValidIssuers = new[] { "accounts.google.com", "https://accounts.google.com" }, ValidateAudience = true, ValidAudience = audience, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKeys = config.SigningKeys, ClockSkew = TimeSpan.FromMinutes(2), }, out _); var subject = principal.FindFirst("sub")?.Value?.Trim() ?? principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value?.Trim(); if (string.IsNullOrWhiteSpace(subject)) { throw new InvalidOperationException("Google token is missing a subject."); } return new GoogleTokenPrincipal( Subject: subject, Email: principal.FindFirst("email")?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.Email)?.Value?.Trim(), EmailVerified: IsEmailVerified(principal), GivenName: principal.FindFirst("given_name")?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.GivenName)?.Value?.Trim(), FamilyName: principal.FindFirst("family_name")?.Value?.Trim() ?? principal.FindFirst(ClaimTypes.Surname)?.Value?.Trim(), Name: principal.FindFirst("name")?.Value?.Trim() ?? principal.Identity?.Name?.Trim() ); } private static bool IsEmailVerified(System.Security.Claims.ClaimsPrincipal principal) { var raw = principal.FindFirst("email_verified")?.Value?.Trim(); if (string.IsNullOrWhiteSpace(raw)) return false; return string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase) || raw == "1"; } }