UDT-002: Logout + Refresh Token con rotación y chain revocation #3

Merged
dmolinari merged 36 commits from feature/UDT-002 into main 2026-04-14 17:37:47 +00:00
Showing only changes of commit a363e3658d - Show all commits

View File

@@ -122,6 +122,65 @@ public class JwtServiceTests : IDisposable
Assert.Equal("2", parsed2.Subject); Assert.Equal("2", parsed2.Subject);
} }
// T-040: GetPrincipalFromExpiredToken
[Fact]
public void GetPrincipalFromExpiredToken_ValidSignatureExpired_ReturnsPrincipal()
{
// Generate a token that will expire in 1 second, then manually create an expired JWT
// using JwtSecurityTokenHandler directly (bypassing the service to control timestamps).
var signingKey = new RsaSecurityKey(_rsa);
var credentials = new SigningCredentials(signingKey, SecurityAlgorithms.RsaSha256);
var past = DateTime.UtcNow.AddHours(-2);
var descriptor = new SecurityTokenDescriptor
{
Subject = new System.Security.Claims.ClaimsIdentity(
[new System.Security.Claims.Claim("sub", "1")]),
Issuer = "sigcm2.api",
Audience = "sigcm2.web",
IssuedAt = past,
NotBefore = past,
Expires = past.AddMinutes(60), // expired 1h ago
SigningCredentials = credentials,
};
var handler = new JwtSecurityTokenHandler();
var token = handler.CreateToken(descriptor);
var expiredToken = handler.WriteToken(token);
// Now use the service — it must validate the signature but ignore expiry
var principal = _jwtService.GetPrincipalFromExpiredToken(expiredToken);
Assert.NotNull(principal);
// The JWT handler maps "sub" to ClaimTypes.NameIdentifier by default,
// but our JwtService uses a custom "sub" claim. Check both.
var sub = principal.FindFirst("sub")?.Value
?? principal.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
Assert.Equal("1", sub);
}
[Fact]
public void GetPrincipalFromExpiredToken_InvalidSignature_Throws()
{
// Sign with a different RSA key
using var otherRsa = System.Security.Cryptography.RSA.Create(2048);
var otherOptions = new JwtOptions { Issuer = "sigcm2.api", Audience = "sigcm2.web", AccessTokenMinutes = 60 };
var otherService = new JwtService(otherRsa, otherOptions);
var tokenFromOtherKey = otherService.GenerateAccessToken(MakeUsuario());
// Validating with the correct key should throw
Assert.Throws<Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException>(
() => _jwtService.GetPrincipalFromExpiredToken(tokenFromOtherKey));
}
[Fact]
public void GetPrincipalFromExpiredToken_MalformedToken_Throws()
{
Assert.ThrowsAny<Exception>(
() => _jwtService.GetPrincipalFromExpiredToken("not.a.valid.jwt"));
}
private static Usuario MakeUsuario(int id = 1, string username = "admin") private static Usuario MakeUsuario(int id = 1, string username = "admin")
=> new(id, username, "$2a$12$hash", "Administrador", "Sistema", null, "admin", "[\"*\"]", true); => new(id, username, "$2a$12$hash", "Administrador", "Sistema", null, "admin", "[\"*\"]", true);
} }