using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Nuuru.Server.Auth; using Nuuru.Server.Models; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; namespace Nuuru.Server.Services { public interface ITokenService { Task GenerateTokenAsync(ApplicationUser user); Task GenerateRefreshTokenAsync(ApplicationUser user); Task GetAccessTokenFromRefreshTokenAsync(string refreshToken); Task RevokeRefreshTokenAsync(string refreshToken); Task RevokeAllRefreshTokensForUserAsync(Guid userId); } public class TokenService : ITokenService { private readonly IConfiguration _configuration; private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly Data.ApplicationDbContext _dbContext; public TokenService( IConfiguration configuration, UserManager userManager, RoleManager roleManager, Data.ApplicationDbContext dbContext) { _configuration = configuration; _userManager = userManager; _roleManager = roleManager; _dbContext = dbContext; } public async Task GenerateTokenAsync(ApplicationUser user) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_configuration["Jwt:Key"]); var claims = new List { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) }; // Add user roles as claims var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } // Get role claims var roleClaims = new List(); foreach (var roleName in roles) { var role = await _roleManager.FindByNameAsync(roleName); if (role != null) { var claims2 = await _roleManager.GetClaimsAsync(role); roleClaims.AddRange(claims2); } } // Get user claims var userClaims = await _userManager.GetClaimsAsync(user); // Compute effective permissions using utility var effectivePermissions = PermissionCalculator.ComputeEffectivePermissionsFromClaims( roleClaims, userClaims); // Add effective permissions to token foreach (var permission in effectivePermissions) { claims.Add(new Claim(Permissions.ClaimType, permission)); } var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(claims), Expires = DateTime.UtcNow.AddMinutes(15), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature), Issuer = _configuration["Jwt:Issuer"], Audience = _configuration["Jwt:Audience"] }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } public async Task GenerateRefreshTokenAsync(ApplicationUser user) { // Generate refresh token (long-lived) var refreshToken = GenerateSecureRandomToken(); var refreshTokenEntity = new RefreshToken { Id = Guid.NewGuid(), Token = refreshToken, UserId = user.Id, ExpiresAt = DateTime.UtcNow.AddDays(7), CreatedAt = DateTime.UtcNow, IsRevoked = false }; _dbContext.RefreshTokens.Add(refreshTokenEntity); await _dbContext.SaveChangesAsync(); return refreshToken; } public async Task GetAccessTokenFromRefreshTokenAsync(string refreshToken) { var storedToken = await _dbContext.RefreshTokens .Include(rt => rt.User) .FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (storedToken == null || storedToken.ExpiresAt < DateTime.UtcNow) { return null; } // Grace window: if a revoked token is re-presented within 30s of rotation // (dropped response on flaky connection, multi-tab race), return the existing // replacement idempotently — no re-rotation, same answer every time. if (storedToken.IsRevoked) { if (storedToken.RevokedAt.HasValue && storedToken.RevokedAt.Value > DateTime.UtcNow.AddSeconds(-30) && !string.IsNullOrEmpty(storedToken.ReplacedByToken)) { var replacementToken = await _dbContext.RefreshTokens .Include(rt => rt.User) .FirstOrDefaultAsync(rt => rt.Token == storedToken.ReplacedByToken); if (replacementToken != null && !replacementToken.IsRevoked && replacementToken.ExpiresAt > DateTime.UtcNow) { // Validate security stamp — same check as normal path var graceStamp = await _userManager.GetSecurityStampAsync(replacementToken.User); if (string.IsNullOrEmpty(graceStamp)) return null; var graceAccessToken = await GenerateTokenAsync(replacementToken.User); return new DTOs.Auth.TokenResponse { AccessToken = graceAccessToken, RefreshToken = replacementToken.Token, ExpiresAt = DateTime.UtcNow.AddMinutes(15) }; } } return null; } // Validate security stamp - ensures token is invalidated if password changed, user locked out, etc. var currentStamp = await _userManager.GetSecurityStampAsync(storedToken.User); if (string.IsNullOrEmpty(currentStamp)) { return null; // User is invalid or was deleted } // Revoke the old refresh token (rotation) storedToken.IsRevoked = true; storedToken.RevokedAt = DateTime.UtcNow; // Generate new access token var accessToken = await GenerateTokenAsync(storedToken.User); // Generate new refresh token (rotation) var newRefreshToken = GenerateSecureRandomToken(); var newRefreshTokenEntity = new RefreshToken { Id = Guid.NewGuid(), Token = newRefreshToken, UserId = storedToken.User.Id, ExpiresAt = DateTime.UtcNow.AddDays(7), CreatedAt = DateTime.UtcNow, IsRevoked = false }; // Link old token to new token storedToken.ReplacedByToken = newRefreshToken; _dbContext.RefreshTokens.Add(newRefreshTokenEntity); await _dbContext.SaveChangesAsync(); return new DTOs.Auth.TokenResponse { AccessToken = accessToken, RefreshToken = newRefreshToken, ExpiresAt = DateTime.UtcNow.AddMinutes(15) }; } public async Task RevokeRefreshTokenAsync(string refreshToken) { var storedToken = await _dbContext.RefreshTokens .FirstOrDefaultAsync(rt => rt.Token == refreshToken); if (storedToken == null) { return false; } if (!storedToken.IsRevoked) { storedToken.IsRevoked = true; storedToken.RevokedAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); } return true; } public async Task RevokeAllRefreshTokensForUserAsync(Guid userId) { var activeTokens = await _dbContext.RefreshTokens .Where(rt => rt.UserId == userId && !rt.IsRevoked) .ToListAsync(); if (activeTokens.Count == 0) { return 0; } var now = DateTime.UtcNow; foreach (var token in activeTokens) { token.IsRevoked = true; token.RevokedAt = now; } await _dbContext.SaveChangesAsync(); return activeTokens.Count; } private static string GenerateSecureRandomToken() { var randomBytes = new byte[64]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomBytes); return Convert.ToBase64String(randomBytes); } } }