| | 1 | | using System.IdentityModel.Tokens.Jwt; |
| | 2 | | using System.Security.Claims; |
| | 3 | | using System.Security.Cryptography; |
| | 4 | | using System.Text; |
| | 5 | | using Microsoft.EntityFrameworkCore; |
| | 6 | | using Microsoft.IdentityModel.Tokens; |
| | 7 | | using Backend.Model; |
| | 8 | |
|
| | 9 | | namespace Backend.Services.Impl |
| | 10 | | { |
| | 11 | | public class TokenService : ITokenService |
| | 12 | | { |
| | 13 | | private readonly IConfiguration _config; |
| | 14 | | private readonly PlannerContext _context; |
| | 15 | | private readonly ILogger<TokenService> _logger; |
| 12 | 16 | | private readonly string _resetStr = "reset"; |
| 12 | 17 | | public TokenService(IConfiguration config, PlannerContext context, ILogger<TokenService> logger) |
| | 18 | | { |
| 12 | 19 | | _config = config; |
| 12 | 20 | | _context = context; |
| 12 | 21 | | _logger = logger; |
| 12 | 22 | | } |
| | 23 | |
|
| | 24 | | public string GenerateAccessToken(User user) |
| | 25 | | { |
| 1 | 26 | | var claims = new[] |
| 1 | 27 | | { |
| 1 | 28 | | new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), |
| 1 | 29 | | new Claim(ClaimTypes.Name, user.Email) |
| 1 | 30 | | }; |
| | 31 | |
|
| 1 | 32 | | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"] ?? "")); |
| 1 | 33 | | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); |
| | 34 | |
|
| 1 | 35 | | var token = new JwtSecurityToken( |
| 1 | 36 | | issuer: _config["Jwt:Issuer"], |
| 1 | 37 | | audience: _config["Jwt:Audience"], |
| 1 | 38 | | claims: claims, |
| 1 | 39 | | expires: DateTime.UtcNow.AddMinutes(int.Parse(_config["Jwt:ExpireMinutes"] ?? "15")), |
| 1 | 40 | | signingCredentials: creds |
| 1 | 41 | | ); |
| | 42 | |
|
| 1 | 43 | | _logger.LogInformation("Generated JWT token for user {UserId} at {Time}", user.Id, DateTime.UtcNow); |
| 1 | 44 | | return new JwtSecurityTokenHandler().WriteToken(token); |
| | 45 | | } |
| | 46 | |
|
| | 47 | | public async Task<RefreshToken> GenerateRefreshToken(User user, string ipAddress) |
| | 48 | | { |
| 1 | 49 | | var randomBytes = new byte[64]; |
| 1 | 50 | | using var rng = RandomNumberGenerator.Create(); |
| 1 | 51 | | rng.GetBytes(randomBytes); |
| 1 | 52 | | var refreshToken = new RefreshToken |
| 1 | 53 | | { |
| 1 | 54 | | Token = Convert.ToBase64String(randomBytes), |
| 1 | 55 | | Expires = DateTime.UtcNow.AddDays(int.Parse(_config["Jwt:RefreshExpireDays"] ?? "7")), |
| 1 | 56 | | Created = DateTime.UtcNow, |
| 1 | 57 | | UserId = user.Id, |
| 1 | 58 | | CreatedByIp = ipAddress, |
| 1 | 59 | | }; |
| | 60 | |
|
| 1 | 61 | | _context.RefreshTokens.Add(refreshToken); |
| 1 | 62 | | await _context.SaveChangesAsync(); |
| | 63 | |
|
| 1 | 64 | | _logger.LogInformation("Generated refresh token for user {UserId} at {Time}", user.Id, DateTime.UtcNow); |
| | 65 | |
|
| 1 | 66 | | return refreshToken; |
| 1 | 67 | | } |
| | 68 | |
|
| | 69 | | public async Task<RefreshToken?> FindRefreshToken(string tokenStr) |
| | 70 | | { |
| 3 | 71 | | var token = await _context.RefreshTokens.FirstOrDefaultAsync(t => t.Token == tokenStr); |
| 3 | 72 | | _logger.LogInformation("Found refresh token for user {UserId} at {Time}", token?.UserId, DateTime.UtcNow); |
| 3 | 73 | | return token; |
| 3 | 74 | | } |
| | 75 | |
|
| | 76 | | public async Task RevokeRefreshToken(RefreshToken token) |
| | 77 | | { |
| 1 | 78 | | token.IsRevoked = true; |
| 1 | 79 | | _context.RefreshTokens.Update(token); |
| 1 | 80 | | await _context.SaveChangesAsync(); |
| 1 | 81 | | _logger.LogInformation("Revoked refresh token for user {UserId} at {Time}", token.UserId, DateTime.UtcNow); |
| 1 | 82 | | } |
| | 83 | |
|
| | 84 | | public string GenerateResetToken(User user) |
| | 85 | | { |
| 3 | 86 | | var tokenHandler = new JwtSecurityTokenHandler(); |
| 3 | 87 | | var key = Encoding.UTF8.GetBytes(_config["Jwt:Key"] ?? "default_secret_key"); |
| | 88 | |
|
| 3 | 89 | | var claims = new[] |
| 3 | 90 | | { |
| 3 | 91 | | new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), |
| 3 | 92 | | new Claim(_resetStr, "true") // custom claim to mark as reset token |
| 3 | 93 | | }; |
| | 94 | |
|
| 3 | 95 | | var tokenDescriptor = new SecurityTokenDescriptor |
| 3 | 96 | | { |
| 3 | 97 | | Subject = new ClaimsIdentity(claims), |
| 3 | 98 | | Expires = DateTime.UtcNow.AddHours(1), // token valid for 1 hour |
| 3 | 99 | | SigningCredentials = new SigningCredentials( |
| 3 | 100 | | new SymmetricSecurityKey(key), |
| 3 | 101 | | SecurityAlgorithms.HmacSha256Signature |
| 3 | 102 | | ), |
| 3 | 103 | | Audience = _config["Jwt:Audience"], |
| 3 | 104 | | Issuer = _config["Jwt:Issuer"] |
| 3 | 105 | | }; |
| | 106 | |
|
| 3 | 107 | | var token = tokenHandler.CreateToken(tokenDescriptor); |
| 3 | 108 | | return tokenHandler.WriteToken(token); |
| | 109 | | } |
| | 110 | |
|
| | 111 | | public int? ValidateResetToken(string token) |
| | 112 | | { |
| 6 | 113 | | var tokenHandler = new JwtSecurityTokenHandler(); |
| 6 | 114 | | var key = Encoding.UTF8.GetBytes(_config["Jwt:Key"] ?? "default_secret_key"); |
| | 115 | |
|
| | 116 | | try |
| | 117 | | { |
| 6 | 118 | | var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters |
| 6 | 119 | | { |
| 6 | 120 | | ValidateIssuerSigningKey = true, |
| 6 | 121 | | IssuerSigningKey = new SymmetricSecurityKey(key), |
| 6 | 122 | | ValidateIssuer = true, |
| 6 | 123 | | ValidIssuer = _config["Jwt:Issuer"], |
| 6 | 124 | | ValidateAudience = true, |
| 6 | 125 | | ValidAudience = _config["Jwt:Audience"], |
| 6 | 126 | | ValidateLifetime = true, |
| 6 | 127 | | ClockSkew = TimeSpan.Zero |
| 6 | 128 | | }, out var validatedToken); |
| | 129 | |
|
| | 130 | | // Make sure it's actually a reset token |
| 4 | 131 | | var resetClaim = principal.FindFirst(_resetStr)?.Value; |
| 4 | 132 | | if (resetClaim != "true") |
| | 133 | | { |
| 1 | 134 | | _logger.LogWarning("Invalid reset token: {Token}", token); |
| 1 | 135 | | return null; |
| | 136 | | } |
| | 137 | |
|
| 3 | 138 | | var sub = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; |
| | 139 | |
|
| 3 | 140 | | int.TryParse(sub, out var userId); |
| 3 | 141 | | if (userId <= 0) |
| | 142 | | { |
| 2 | 143 | | _logger.LogWarning("Invalid user ID in reset token: {Token}", token); |
| 2 | 144 | | return null; |
| | 145 | | } |
| | 146 | |
|
| 1 | 147 | | return userId; |
| | 148 | | } |
| 2 | 149 | | catch (Exception ex) |
| | 150 | | { |
| 2 | 151 | | _logger.LogError(ex, "Error validating reset token: {Token}", token); |
| 2 | 152 | | return null; // invalid or expired token |
| | 153 | | } |
| 6 | 154 | | } |
| | 155 | | } |
| | 156 | | } |