using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; namespace JobTrackerApi.Services; public sealed record GoogleTokenPrincipal(string Subject, string? Email, 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 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(); if (string.IsNullOrWhiteSpace(subject)) { throw new InvalidOperationException("Google token is missing a subject."); } return new GoogleTokenPrincipal( Subject: subject, Email: principal.FindFirst("email")?.Value?.Trim(), GivenName: principal.FindFirst("given_name")?.Value?.Trim(), FamilyName: principal.FindFirst("family_name")?.Value?.Trim(), Name: principal.FindFirst("name")?.Value?.Trim() ); } }