Files
TenWholeYears.Server/RecNet.Infrastructure/Services/Tokens/TokenService.cs
2026-06-19 15:12:04 -05:00

97 lines
3.3 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using RecNet.Application.Common.Interfaces;
using RecNet.Application.Common.Tokens;
using RecNet.Domain.Profiles;
namespace RecNet.Infrastructure.Services.Tokens;
public class TokenService(IOptions<JwtOptions> options) : ITokenService
{
public TokenResult GenerateProfileToken(Profile profile)
{
var jwtOptions = options.Value;
if (string.IsNullOrWhiteSpace(jwtOptions.Secret))
throw new InvalidOperationException("JWT secret is not configured.");
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(jwtOptions.ExpiryMinutes);
var signingCredentials = new SigningCredentials(
CreateSecurityKey(jwtOptions),
SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, profile.ProfileId.ToString()),
new("rn.plat", ((int)profile.Platform).ToString()),
new("rn.platid", profile.PlatformId)
};
if (profile.IsModerator)
claims.Add(new Claim("role", "moderator"));
var token = new JwtSecurityToken(
issuer: jwtOptions.Issuer,
audience: jwtOptions.Audience,
claims: claims,
expires: expiresAt.UtcDateTime,
signingCredentials: signingCredentials);
return new TokenResult(
new JwtSecurityTokenHandler().WriteToken(token),
jwtOptions.ExpiryMinutes * 60);
}
public TokenVerifyResult VerifyToken(string token)
{
var jwtOptions = options.Value;
if (string.IsNullOrWhiteSpace(jwtOptions.Secret))
throw new InvalidOperationException("JWT secret is not configured.");
if (string.IsNullOrWhiteSpace(token))
return TokenVerifyResult.Failure();
var tokenHandler = new JwtSecurityTokenHandler();
if (!tokenHandler.CanReadToken(token))
return TokenVerifyResult.Failure();
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = CreateSecurityKey(jwtOptions),
ValidateIssuer = true,
ValidIssuer = jwtOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtOptions.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
try
{
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
var profileIdClaim =
principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ??
principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Guid.TryParse(profileIdClaim, out var profileId)
? TokenVerifyResult.Success(profileId)
: TokenVerifyResult.Failure();
}
catch (SecurityTokenException)
{
return TokenVerifyResult.Failure();
}
catch (ArgumentException)
{
return TokenVerifyResult.Failure();
}
}
private static SymmetricSecurityKey CreateSecurityKey(JwtOptions jwtOptions)
=> new(Encoding.UTF8.GetBytes(jwtOptions.Secret));
}