Files

82 lines
3.5 KiB
C#

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<GoogleTokenPrincipal> ValidateAsync(string idToken, CancellationToken cancellationToken = default);
}
public sealed class GoogleTokenValidator : IGoogleTokenValidator
{
private readonly IConfiguration _cfg;
private readonly IConfigurationManager<OpenIdConnectConfiguration> _configManager;
public GoogleTokenValidator(IConfiguration cfg)
{
_cfg = cfg;
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
"https://accounts.google.com/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
}
public GoogleTokenValidator(IConfiguration cfg, IConfigurationManager<OpenIdConnectConfiguration> configManager)
{
_cfg = cfg;
_configManager = configManager;
}
public async Task<GoogleTokenPrincipal> 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";
}
}