128 lines
4.5 KiB
C#
128 lines
4.5 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Security.Cryptography;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using SIGCM2.Domain.Entities;
|
|
using SIGCM2.Infrastructure.Security;
|
|
|
|
namespace SIGCM2.Application.Tests.Infrastructure;
|
|
|
|
public class JwtServiceTests : IDisposable
|
|
{
|
|
private readonly RSA _rsa;
|
|
private readonly JwtOptions _options;
|
|
private readonly JwtService _jwtService;
|
|
|
|
public JwtServiceTests()
|
|
{
|
|
// Generate a test RSA key pair inline (no files needed for unit tests)
|
|
_rsa = RSA.Create(2048);
|
|
_options = new JwtOptions
|
|
{
|
|
Issuer = "sigcm2.api",
|
|
Audience = "sigcm2.web",
|
|
AccessTokenMinutes = 60
|
|
};
|
|
_jwtService = new JwtService(_rsa, _options);
|
|
}
|
|
|
|
public void Dispose() => _rsa.Dispose();
|
|
|
|
// Scenario: generated token uses RS256 algorithm
|
|
[Fact]
|
|
public void GenerateAccessToken_UsesRS256Algorithm()
|
|
{
|
|
var usuario = MakeUsuario();
|
|
var token = _jwtService.GenerateAccessToken(usuario);
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var parsed = handler.ReadJwtToken(token);
|
|
|
|
Assert.Equal("RS256", parsed.Header.Alg);
|
|
}
|
|
|
|
// Scenario: claims contain expected values
|
|
[Fact]
|
|
public void GenerateAccessToken_ContainsExpectedClaims()
|
|
{
|
|
var usuario = MakeUsuario();
|
|
var token = _jwtService.GenerateAccessToken(usuario);
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var parsed = handler.ReadJwtToken(token);
|
|
|
|
Assert.Equal("1", parsed.Subject); // sub = user ID
|
|
Assert.Equal("sigcm2.api", parsed.Issuer); // iss
|
|
Assert.Contains("sigcm2.web", parsed.Audiences); // aud
|
|
Assert.Contains(parsed.Claims, c => c.Type == "name" && c.Value == "admin");
|
|
Assert.Contains(parsed.Claims, c => c.Type == "rol" && c.Value == "admin");
|
|
}
|
|
|
|
// Scenario: token is verifiable with the public key
|
|
[Fact]
|
|
public void GenerateAccessToken_IsVerifiableWithPublicKey()
|
|
{
|
|
var usuario = MakeUsuario();
|
|
var token = _jwtService.GenerateAccessToken(usuario);
|
|
|
|
var publicKey = RSA.Create();
|
|
publicKey.ImportRSAPublicKey(_rsa.ExportRSAPublicKey(), out _);
|
|
|
|
var validationParams = new TokenValidationParameters
|
|
{
|
|
ValidateIssuerSigningKey = true,
|
|
IssuerSigningKey = new RsaSecurityKey(publicKey),
|
|
ValidIssuer = "sigcm2.api",
|
|
ValidAudience = "sigcm2.web",
|
|
ValidateLifetime = true,
|
|
ClockSkew = TimeSpan.Zero
|
|
};
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var principal = handler.ValidateToken(token, validationParams, out var validatedToken);
|
|
|
|
Assert.NotNull(principal);
|
|
Assert.IsType<JwtSecurityToken>(validatedToken);
|
|
}
|
|
|
|
// Scenario: expiry is 60 minutes from now
|
|
[Fact]
|
|
public void GenerateAccessToken_ExpiryIs60MinutesFromNow()
|
|
{
|
|
var usuario = MakeUsuario();
|
|
var before = DateTime.UtcNow;
|
|
var token = _jwtService.GenerateAccessToken(usuario);
|
|
var after = DateTime.UtcNow;
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var parsed = handler.ReadJwtToken(token);
|
|
|
|
var expectedMinExpiry = before.AddMinutes(59).AddSeconds(55);
|
|
var expectedMaxExpiry = after.AddMinutes(60).AddSeconds(5);
|
|
|
|
Assert.True(parsed.ValidTo >= expectedMinExpiry, $"exp {parsed.ValidTo} < expected min {expectedMinExpiry}");
|
|
Assert.True(parsed.ValidTo <= expectedMaxExpiry, $"exp {parsed.ValidTo} > expected max {expectedMaxExpiry}");
|
|
}
|
|
|
|
// Triangulation: different user produces token with different sub claim
|
|
[Fact]
|
|
public void GenerateAccessToken_DifferentUser_DifferentSubClaim()
|
|
{
|
|
var user1 = MakeUsuario(id: 1, username: "admin");
|
|
var user2 = MakeUsuario(id: 2, username: "vendedor");
|
|
|
|
var token1 = _jwtService.GenerateAccessToken(user1);
|
|
var token2 = _jwtService.GenerateAccessToken(user2);
|
|
|
|
var handler = new JwtSecurityTokenHandler();
|
|
var parsed1 = handler.ReadJwtToken(token1);
|
|
var parsed2 = handler.ReadJwtToken(token2);
|
|
|
|
Assert.NotEqual(parsed1.Subject, parsed2.Subject);
|
|
Assert.Equal("1", parsed1.Subject);
|
|
Assert.Equal("2", parsed2.Subject);
|
|
}
|
|
|
|
private static Usuario MakeUsuario(int id = 1, string username = "admin")
|
|
=> new(id, username, "$2a$12$hash", "Administrador", "Sistema", null, "admin", "[\"*\"]", true);
|
|
}
|