using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using MealPlan.Api.Models; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace MealPlan.Api.Services; /// /// Issues JWT access tokens and rotating refresh tokens. /// Refresh tokens are cryptographically random opaque strings tracked in . /// public class TokenService : ITokenService { private readonly JwtOptions _options; private readonly IRefreshTokenStore _refreshTokenStore; public TokenService(IOptions options, IRefreshTokenStore refreshTokenStore) { _options = options.Value; _refreshTokenStore = refreshTokenStore; } public int AccessTokenLifetimeSeconds => _options.AccessTokenMinutes * 60; public string CreateAccessToken(ApplicationUser user) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Secret)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var userId = user.Id.ToString(); var claims = new List { new(JwtRegisteredClaimNames.Sub, userId), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(ClaimTypes.NameIdentifier, userId), }; if (!string.IsNullOrWhiteSpace(user.Email)) { claims.Add(new Claim(JwtRegisteredClaimNames.Email, user.Email)); } if (!string.IsNullOrWhiteSpace(user.UserName)) { claims.Add(new Claim(ClaimTypes.Name, user.UserName)); } var token = new JwtSecurityToken( issuer: _options.Issuer, audience: _options.Audience, claims: claims, notBefore: DateTime.UtcNow, expires: DateTime.UtcNow.AddMinutes(_options.AccessTokenMinutes), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } public RefreshToken CreateRefreshToken(string userId) { var token = new RefreshToken { Token = GenerateSecureToken(), UserId = userId, CreatedAtUtc = DateTime.UtcNow, ExpiresAtUtc = DateTime.UtcNow.AddDays(_options.RefreshTokenDays), IsRevoked = false, }; _refreshTokenStore.Add(token); return token; } private static string GenerateSecureToken() { var bytes = RandomNumberGenerator.GetBytes(64); return Convert.ToBase64String(bytes); } }