w4c-workflows-api/w4c-workflows-api.Tests/JwtValidatorTests.cs

97 lines
3.4 KiB
C#
Raw Normal View History

2026-09-13 16:28:47 +00:00
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using w4c_workflows.Services;
using Xunit;
namespace w4c_workflows.Tests;
/// <summary>
/// P1-14: the shared symmetric key alone is weak, so issuer/audience must be
/// enforced whenever the deployment declares them, and left optional otherwise
/// (w4c-auth may not stamp them). A token signed with the wrong key, from the
/// wrong issuer or for the wrong audience must never validate.
/// </summary>
public class JwtValidatorTests
{
private const string SigningKey = "unit-test-signing-key-that-is-long-enough-1234567890";
private const string OtherKey = "another-signing-key-that-is-long-enough-0987654321";
private static string Token(
string key,
string? issuer = "w4c-auth",
string? audience = "w4c",
DateTime? expires = null,
DateTime? notBefore = null)
{
var credentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: new[] { new Claim("tenant_id", "tenant-42"), new Claim("sub", "user-1") },
notBefore: notBefore ?? DateTime.UtcNow.AddMinutes(-1),
expires: expires ?? DateTime.UtcNow.AddMinutes(5),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
[Fact]
public void Valid_token_is_accepted_and_extracts_the_tenant()
{
var principal = JwtValidator.Validate($"Bearer {Token(SigningKey)}", SigningKey, "w4c-auth", "w4c");
Assert.NotNull(principal);
Assert.Equal("tenant-42", principal!.FindFirst("tenant_id")?.Value);
}
[Fact]
public void Issuer_and_audience_are_enforced_when_configured()
{
var bearer = $"Bearer {Token(SigningKey, issuer: "attacker", audience: "w4c")}";
Assert.Null(JwtValidator.Validate(bearer, SigningKey, "w4c-auth", "w4c"));
bearer = $"Bearer {Token(SigningKey, issuer: "w4c-auth", audience: "someone-else")}";
Assert.Null(JwtValidator.Validate(bearer, SigningKey, "w4c-auth", "w4c"));
}
[Fact]
public void Issuer_and_audience_are_optional_when_the_deployment_does_not_declare_them()
{
// A token that carries an issuer/audience still validates when the server
// is not configured to check them (backward compatibility with w4c-auth).
var principal = JwtValidator.Validate($"Bearer {Token(SigningKey)}", SigningKey);
Assert.NotNull(principal);
}
[Fact]
public void Token_signed_with_another_key_is_rejected()
{
Assert.Null(JwtValidator.Validate($"Bearer {Token(OtherKey)}", SigningKey, "w4c-auth", "w4c"));
}
[Fact]
public void Expired_token_is_rejected()
{
var expired = Token(
SigningKey,
notBefore: DateTime.UtcNow.AddHours(-1),
expires: DateTime.UtcNow.AddMinutes(-5));
Assert.Null(JwtValidator.Validate($"Bearer {expired}", SigningKey, "w4c-auth", "w4c"));
}
[Theory]
[InlineData("")]
[InlineData("not-a-bearer")]
[InlineData("Bearer ")]
public void Missing_or_malformed_header_is_rejected(string header)
{
Assert.Null(JwtValidator.Validate(header, SigningKey, "w4c-auth", "w4c"));
}
}