using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Renis.Database.Models; using Microsoft.IdentityModel.Tokens; using Renis.Repositories; namespace Renis.Services.Jwt; public class JwtService : IJwtService { private readonly string _secretKey; private readonly string _issuer; private readonly string _audience; private readonly IUserRepository _userRepo; private readonly ILogger _logger; public JwtService(ILogger logger, IUserRepository userRepo) { _logger = logger; _userRepo = userRepo; // TODO: Change issuer & audience _issuer = Environment.GetEnvironmentVariable("AUTH_JWT_ISSUER") ?? "renis"; _audience = Environment.GetEnvironmentVariable("AUTH_JWT_AUDIENCE") ?? "renis"; _secretKey = Environment.GetEnvironmentVariable("AUTH_JWT_SECRET")!; if (_secretKey == null) { throw new Exception("Missing AUTH_JWT_SECRET environment variable"); } } /// /// Generates an access token for the specified user. /// /// The user for whom the access token is generated. /// The generated access token as a string. public string GenerateAccessToken(User user) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Convert.FromBase64String(_secretKey); var tokenDescriptor = new SecurityTokenDescriptor { Issuer = _issuer, Audience = _audience, Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Phone), new Claim(ClaimsIdentity.DefaultRoleClaimType,"User"), new Claim(ClaimTypes.AuthenticationMethod, "Access") }), Expires = DateTime.UtcNow.AddMinutes(50), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } /// /// Generates a refresh token for the specified user. /// /// The user for whom the refresh token is generated. /// The generated refresh token as a string. public string GenerateRefreshToken(User user) { var tokenHandler = new JwtSecurityTokenHandler(); var key = Convert.FromBase64String(_secretKey); var tokenDescriptor = new SecurityTokenDescriptor { Issuer = _issuer, Audience = _audience, Subject = new ClaimsIdentity(new[] { new Claim("PasswordChangeDate", user.PasswordChangeDate.ToString()), new Claim(ClaimTypes.Name, user.Phone), new Claim(ClaimsIdentity.DefaultRoleClaimType,"User"), new Claim(ClaimTypes.AuthenticationMethod, "Refresh") }), Expires = DateTime.UtcNow.AddDays(7), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } /// /// Validates the access token and extracts the username from it if valid. /// Returns a tuple indicating whether the token is valid and the extracted username. /// /// The access token to validate. /// A tuple indicating whether the token is valid and the extracted username. public Tuple ValidateAccessToken(string? token) { try { if (token == null) { _logger.LogWarning("JwtService: No access token string provided"); return new (false, ""); } var tokenHandler = new JwtSecurityTokenHandler(); var key = Convert.FromBase64String(_secretKey); TokenValidationParameters validationParameters = new() { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = true, ValidIssuer = _issuer, ValidateAudience = true, ValidAudience = _audience, ClockSkew = TimeSpan.Zero }; // Валидация токена tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); JwtSecurityToken validatedJwt = (JwtSecurityToken)validatedToken; var username = validatedJwt.Claims.First(claim => claim.Type == "unique_name").Value; // Проверка типа токена if (validatedJwt.Claims.First(claim => claim.Type == ClaimTypes.AuthenticationMethod).Value != "Access") { _logger.LogWarning("JwtService: the token was not a access token"); return new (false, ""); } return new (true, username); } catch (Exception e) { _logger.LogWarning("JwtService: Token invalidated due to exception:\n{Exception}", e); return new (false, "");; } } /// /// Validates the refresh token and extracts the username from it if valid. /// Returns a tuple indicating whether the token is valid and the extracted username. /// /// The refresh token to validate. /// A tuple indicating whether the token is valid and the extracted username. public async Task> ValidateRefreshToken(string? token) { try { if (token == null) { _logger.LogWarning("JwtService: No refresh token string provided"); return new (false, "");; } var tokenHandler = new JwtSecurityTokenHandler(); var key = Convert.FromBase64String(_secretKey); TokenValidationParameters validationParameters = new() { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = true, ValidIssuer = _issuer, ValidateAudience = true, ValidAudience = _audience, ClockSkew = TimeSpan.Zero }; // Валидация токена tokenHandler.ValidateToken(token, validationParameters, out SecurityToken validatedToken); JwtSecurityToken validatedJwt = (JwtSecurityToken)validatedToken; var passwordChangeDate = validatedJwt.Claims.First(claim => claim.Type == "PasswordChangeDate").Value; var username = validatedJwt.Claims.First(claim => claim.Type == "unique_name").Value; // Проверка типа токена if (validatedJwt.Claims.First(claim => claim.Type == "authmethod").Value != "Refresh") { _logger.LogWarning("JwtService: the token was not a refresh token"); return new (false, ""); } var user = await _userRepo.GetUserByPhone(username); if (user == null) { return new (false, ""); } // Проверка, что дата изменения пароля совпадает с фактической if (user.PasswordChangeDate.ToString() != passwordChangeDate) { username=null; _logger.LogWarning("JwtService: the password change date was not equal to the one in the token\n1. {1}\n2. {2}", passwordChangeDate, user.PasswordChangeDate.ToString()); return new (false, ""); } return new (true, username); } catch (Exception e) { _logger.LogWarning("JwtService: Token invalidated due to exception:\n{Exception}", e); return new (false, ""); } } }