feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -13,65 +13,134 @@ namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class MicrosoftTokenValidatorTests
|
||||
{
|
||||
private static (IConfiguration Config, Mock<IConfigurationManager<OpenIdConnectConfiguration>> ConfigManager, SymmetricSecurityKey Key) BuildHarness()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?> { ["Auth:MicrosoftClientId"] = "client-123" })
|
||||
.Build();
|
||||
|
||||
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super-secret-signing-key-super-secret"));
|
||||
var oidc = new OpenIdConnectConfiguration();
|
||||
oidc.SigningKeys.Add(signingKey);
|
||||
|
||||
var configManager = new Mock<IConfigurationManager<OpenIdConnectConfiguration>>();
|
||||
configManager.Setup(x => x.GetConfigurationAsync(It.IsAny<CancellationToken>())).ReturnsAsync(oidc);
|
||||
|
||||
return (config, configManager, signingKey);
|
||||
}
|
||||
private const string TenantA = "11111111-1111-1111-1111-111111111111";
|
||||
private const string TenantB = "22222222-2222-2222-2222-222222222222";
|
||||
private const string ObjectId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_accepts_tenant_scoped_issuer_and_maps_oid_to_subject()
|
||||
public async Task Valid_common_token_returns_the_tenant_object_pair_and_unverified_email_metadata()
|
||||
{
|
||||
var (config, configManager, signingKey) = BuildHarness();
|
||||
var (validator, key) = BuildHarness("common");
|
||||
|
||||
var token = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
|
||||
issuer: "https://login.microsoftonline.com/9f2c1e3a-tenant/v2.0",
|
||||
audience: "client-123",
|
||||
claims: new[]
|
||||
{
|
||||
new Claim("oid", "ms-subject-1"),
|
||||
new Claim("email", "demo@example.com"),
|
||||
new Claim("given_name", "Demo"),
|
||||
new Claim("family_name", "User"),
|
||||
new Claim("name", "Demo User"),
|
||||
},
|
||||
expires: DateTime.UtcNow.AddMinutes(10),
|
||||
signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)));
|
||||
var result = await validator.ValidateAsync(CreateToken(key, TenantA, ObjectId));
|
||||
|
||||
var validator = new MicrosoftTokenValidator(config, configManager.Object);
|
||||
var result = await validator.ValidateAsync(token);
|
||||
|
||||
Assert.Equal("ms-subject-1", result.Subject);
|
||||
Assert.Equal(TenantA, result.TenantId);
|
||||
Assert.Equal(ObjectId, result.ObjectId);
|
||||
Assert.Equal(ObjectId, result.Subject); // Legacy consumer until SEC-004 switches persistence.
|
||||
Assert.Equal("demo@example.com", result.Email);
|
||||
Assert.True(result.EmailVerified);
|
||||
Assert.Equal("Demo", result.GivenName);
|
||||
Assert.Equal("User", result.FamilyName);
|
||||
Assert.False(result.EmailVerified);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_rejects_non_microsoft_issuer()
|
||||
public void Production_requires_an_explicit_tenant_policy()
|
||||
{
|
||||
var (config, configManager, signingKey) = BuildHarness();
|
||||
Assert.Throws<InvalidOperationException>(() => MicrosoftTenantPolicy.Parse(null, production: true));
|
||||
Assert.Equal("common", MicrosoftTenantPolicy.Parse(null, production: false).Mode);
|
||||
Assert.Throws<InvalidOperationException>(() => MicrosoftTenantPolicy.Parse("not-a-tenant", production: false));
|
||||
}
|
||||
|
||||
var token = new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
|
||||
issuer: "https://evil.example.com/v2.0",
|
||||
audience: "client-123",
|
||||
claims: new[] { new Claim("oid", "ms-subject-1") },
|
||||
expires: DateTime.UtcNow.AddMinutes(10),
|
||||
signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)));
|
||||
[Theory]
|
||||
[InlineData("common", TenantA, true)]
|
||||
[InlineData("common", MicrosoftTenantPolicy.ConsumerTenantId, true)]
|
||||
[InlineData("organizations", TenantA, true)]
|
||||
[InlineData("organizations", MicrosoftTenantPolicy.ConsumerTenantId, false)]
|
||||
[InlineData("consumers", MicrosoftTenantPolicy.ConsumerTenantId, true)]
|
||||
[InlineData("consumers", TenantA, false)]
|
||||
[InlineData(TenantA, TenantA, true)]
|
||||
[InlineData(TenantA, TenantB, false)]
|
||||
public async Task Tenant_modes_accept_only_their_documented_accounts(string mode, string tokenTenant, bool accepted)
|
||||
{
|
||||
var (validator, key) = BuildHarness(mode);
|
||||
var token = CreateToken(key, tokenTenant, ObjectId);
|
||||
|
||||
var validator = new MicrosoftTokenValidator(config, configManager.Object);
|
||||
if (accepted)
|
||||
Assert.Equal(tokenTenant, (await validator.ValidateAsync(token)).TenantId);
|
||||
else
|
||||
await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(token));
|
||||
}
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => validator.ValidateAsync(token));
|
||||
[Theory]
|
||||
[InlineData(null, ObjectId)]
|
||||
[InlineData("not-a-guid", ObjectId)]
|
||||
[InlineData(TenantA, null)]
|
||||
[InlineData(TenantA, "not-a-guid")]
|
||||
public async Task Tid_and_oid_are_required_guids(string? tenantId, string? objectId)
|
||||
{
|
||||
var (validator, key) = BuildHarness("common");
|
||||
|
||||
await Assert.ThrowsAnyAsync<Exception>(() => validator.ValidateAsync(CreateToken(key, tenantId, objectId)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Issuer_must_exactly_match_the_signed_tid()
|
||||
{
|
||||
var (validator, key) = BuildHarness("common");
|
||||
var token = CreateToken(key, TenantA, ObjectId, issuer: $"https://login.microsoftonline.com/{TenantB}/v2.0");
|
||||
|
||||
await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Audience_signature_and_lifetime_remain_enforced()
|
||||
{
|
||||
var (validator, key) = BuildHarness("common");
|
||||
var otherKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("different-signing-key-different-signing-key"));
|
||||
|
||||
await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(CreateToken(key, TenantA, ObjectId, audience: "wrong-client")));
|
||||
await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(CreateToken(otherKey, TenantA, ObjectId)));
|
||||
await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(CreateToken(key, TenantA, ObjectId, expires: DateTime.UtcNow.AddMinutes(-10))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Same_oid_in_two_tenants_produces_two_distinct_stable_pairs()
|
||||
{
|
||||
var (validator, key) = BuildHarness("common");
|
||||
|
||||
var first = await validator.ValidateAsync(CreateToken(key, TenantA, ObjectId));
|
||||
var second = await validator.ValidateAsync(CreateToken(key, TenantB, ObjectId));
|
||||
|
||||
Assert.NotEqual(first.TenantId, second.TenantId);
|
||||
Assert.Equal(first.ObjectId, second.ObjectId);
|
||||
}
|
||||
|
||||
private static (MicrosoftTokenValidator Validator, SymmetricSecurityKey Key) BuildHarness(string tenantMode)
|
||||
{
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Auth:MicrosoftClientId"] = "client-123",
|
||||
["Auth:MicrosoftTenant"] = tenantMode,
|
||||
}).Build();
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super-secret-signing-key-super-secret"));
|
||||
var oidc = new OpenIdConnectConfiguration();
|
||||
oidc.SigningKeys.Add(key);
|
||||
var manager = new Mock<IConfigurationManager<OpenIdConnectConfiguration>>();
|
||||
manager.Setup(x => x.GetConfigurationAsync(It.IsAny<CancellationToken>())).ReturnsAsync(oidc);
|
||||
return (new MicrosoftTokenValidator(config, manager.Object), key);
|
||||
}
|
||||
|
||||
private static string CreateToken(
|
||||
SecurityKey key,
|
||||
string? tenantId,
|
||||
string? objectId,
|
||||
string? issuer = null,
|
||||
string audience = "client-123",
|
||||
DateTime? expires = null)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new("email", "demo@example.com"),
|
||||
new("given_name", "Demo"),
|
||||
new("family_name", "User"),
|
||||
};
|
||||
if (tenantId is not null) claims.Add(new Claim("tid", tenantId));
|
||||
if (objectId is not null) claims.Add(new Claim("oid", objectId));
|
||||
issuer ??= $"https://login.microsoftonline.com/{tenantId}/v2.0";
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
|
||||
issuer,
|
||||
audience,
|
||||
claims,
|
||||
expires: expires ?? DateTime.UtcNow.AddMinutes(10),
|
||||
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user