73 lines
2.9 KiB
C#
73 lines
2.9 KiB
C#
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, 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 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();
|
|
if (string.IsNullOrWhiteSpace(subject))
|
|
{
|
|
throw new InvalidOperationException("Google token is missing a subject.");
|
|
}
|
|
|
|
return new GoogleTokenPrincipal(
|
|
Subject: subject,
|
|
Email: principal.FindFirst("email")?.Value?.Trim(),
|
|
EmailVerified: IsEmailVerified(principal),
|
|
GivenName: principal.FindFirst("given_name")?.Value?.Trim(),
|
|
FamilyName: principal.FindFirst("family_name")?.Value?.Trim(),
|
|
Name: principal.FindFirst("name")?.Value?.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";
|
|
}
|
|
}
|