using System.Text; using System.Text.Json; using BuildingBlock.Identity.Contracts.Conventions; using BuildingBlock.Identity.Contracts.Requests; using Thalos.Service.Application.Oidc; using Thalos.Service.Application.Secrets; namespace Thalos.Service.Application.UnitTests; public class GoogleIdentityProviderTokenExchangeServiceTests { [Fact] public async Task ExchangeAsync_WhenTokenClaimsMatch_ReturnsAuthenticatedSubject() { var provider = new FakeSecretMaterialProvider( new Dictionary(StringComparer.Ordinal) { ["Oidc:Google:ClientId"] = "google-client-1", ["Oidc:Google:Issuer"] = "https://accounts.google.com" }); var service = new GoogleIdentityProviderTokenExchangeService(provider); var token = BuildUnsignedJwt(new Dictionary { ["sub"] = "google-sub-1", ["aud"] = "google-client-1", ["iss"] = "https://accounts.google.com" }); var response = await service.ExchangeAsync(new ExchangeIdentityProviderTokenRequest( "tenant-1", IdentityAuthProvider.Google, token, "corr-1")); Assert.True(response.IsAuthenticated); Assert.Equal("google-sub-1", response.SubjectId); } [Fact] public async Task ExchangeAsync_WhenAudienceMismatches_ReturnsUnauthenticated() { var provider = new FakeSecretMaterialProvider( new Dictionary(StringComparer.Ordinal) { ["Oidc:Google:ClientId"] = "google-client-1" }); var service = new GoogleIdentityProviderTokenExchangeService(provider); var token = BuildUnsignedJwt(new Dictionary { ["sub"] = "google-sub-2", ["aud"] = "google-client-2", ["iss"] = "https://accounts.google.com" }); var response = await service.ExchangeAsync(new ExchangeIdentityProviderTokenRequest( "tenant-2", IdentityAuthProvider.Google, token, "corr-2")); Assert.False(response.IsAuthenticated); Assert.Equal(string.Empty, response.SubjectId); } [Fact] public async Task ExchangeAsync_WhenGoogleClientIdSecretMissing_ReturnsUnauthenticated() { var provider = new FakeSecretMaterialProvider(new Dictionary(StringComparer.Ordinal)); var service = new GoogleIdentityProviderTokenExchangeService(provider); var token = BuildUnsignedJwt(new Dictionary { ["sub"] = "google-sub-3", ["aud"] = "google-client-1", ["iss"] = "https://accounts.google.com" }); var response = await service.ExchangeAsync(new ExchangeIdentityProviderTokenRequest( "tenant-3", IdentityAuthProvider.Google, token, "corr-3")); Assert.False(response.IsAuthenticated); Assert.Equal(string.Empty, response.SubjectId); } private static string BuildUnsignedJwt(Dictionary payload) { var header = Base64UrlEncode("""{"alg":"none","typ":"JWT"}"""); var payloadJson = JsonSerializer.Serialize(payload); var payloadEncoded = Base64UrlEncode(payloadJson); return $"{header}.{payloadEncoded}."; } private static string Base64UrlEncode(string value) { var bytes = Encoding.UTF8.GetBytes(value); var encoded = Convert.ToBase64String(bytes); return encoded.TrimEnd('=').Replace('+', '-').Replace('/', '_'); } private sealed class FakeSecretMaterialProvider( IReadOnlyDictionary secrets) : IIdentitySecretMaterialProvider { public bool TryGetSecret(string secretKey, out string secretValue) { if (secrets.TryGetValue(secretKey, out var value) && !string.IsNullOrWhiteSpace(value)) { secretValue = value; return true; } secretValue = string.Empty; return false; } public string GetSecret(string secretKey) { return TryGetSecret(secretKey, out var secretValue) ? secretValue : string.Empty; } } }