Task 5: TokenService + unit tests (7/7 pass)

- ITokenService: GenerateAccessToken / GenerateRefreshToken / HashToken
- TokenService: JWT (HS256, 15-min), 64-byte CSPRNG refresh, SHA-256 hex hash
  - Role claims use short JWT name role (v7.x JsonWebTokenHandler compatible)
- TokenServiceTests: 7 xUnit tests, payload decoded via Base64Url+System.Text.Json
  to avoid Microsoft.IdentityModel 7.1.2/7.5.2 version-mismatch issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-05-26 17:34:56 -07:00
parent b335867b30
commit f74563bb36
4 changed files with 261 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using ROLAC.API.Entities;
namespace ROLAC.API.Services;
public class TokenService : ITokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config)
{
_config = config;
}
public string GenerateAccessToken(AppUser user, IList<string> roles)
{
var secretKey = _config["Jwt:SecretKey"]!;
var issuer = _config["Jwt:Issuer"]!;
var audience = _config["Jwt:Audience"]!;
var expiryMin = int.Parse(_config["Jwt:AccessTokenExpiryMinutes"]!);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id),
new(JwtRegisteredClaimNames.Email, user.Email!),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
// Use the short JWT claim name "role" so the payload is clean and
// JsonWebTokenHandler (the v7.x default validator) can read it without
// needing an inbound claim-type map applied.
foreach (var role in roles)
claims.Add(new Claim("role", role));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(expiryMin),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var bytes = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
public string HashToken(string rawToken)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawToken));
return Convert.ToHexString(bytes).ToLower();
}
}