thalos-service/tests/Thalos.Service.Application.UnitTests/GoogleIdentityProviderTokenExchangeServiceTests.cs
2026-03-11 04:27:02 -06:00

123 lines
4.3 KiB
C#

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<string, string>(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<string, object?>
{
["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<string, string>(StringComparer.Ordinal)
{
["Oidc:Google:ClientId"] = "google-client-1"
});
var service = new GoogleIdentityProviderTokenExchangeService(provider);
var token = BuildUnsignedJwt(new Dictionary<string, object?>
{
["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<string, string>(StringComparer.Ordinal));
var service = new GoogleIdentityProviderTokenExchangeService(provider);
var token = BuildUnsignedJwt(new Dictionary<string, object?>
{
["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<string, object?> 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<string, string> 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;
}
}
}