fix: No token refresh or revocation mechanism~
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -40,10 +41,12 @@ public class AuthService : IAuthService
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var expires = DateTimeOffset.UtcNow.AddMinutes(_jwt.ExpirationMinutes);
|
||||
var token = GenerateToken(user, expires);
|
||||
var accessToken = GenerateAccessToken(user, expires);
|
||||
var refreshToken = await CreateRefreshTokenAsync(user.Id);
|
||||
|
||||
return new LoginResponse(
|
||||
token,
|
||||
accessToken,
|
||||
refreshToken.Token,
|
||||
expires,
|
||||
user.Id,
|
||||
user.Username,
|
||||
@@ -51,7 +54,88 @@ public class AuthService : IAuthService
|
||||
user.Role.ToDbString());
|
||||
}
|
||||
|
||||
private string GenerateToken(ClinicalUser user, DateTimeOffset expires)
|
||||
public async Task<RefreshResponse> RefreshAsync(string refreshToken)
|
||||
{
|
||||
var stored = await _db.RefreshTokens
|
||||
.Include(t => t.User)
|
||||
.FirstOrDefaultAsync(t => t.Token == refreshToken);
|
||||
|
||||
if (stored is null || stored.RevokedAt is not null || stored.ExpiresAt < DateTimeOffset.UtcNow)
|
||||
throw new ValidationException("Invalid or expired refresh token.", "INVALID_REFRESH_TOKEN");
|
||||
|
||||
if (!stored.User.IsActive)
|
||||
throw new ValidationException("Account is deactivated.", "ACCOUNT_DEACTIVATED");
|
||||
|
||||
stored.RevokedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var newRefreshToken = await CreateRefreshTokenAsync(stored.UserId);
|
||||
|
||||
var expires = DateTimeOffset.UtcNow.AddMinutes(_jwt.ExpirationMinutes);
|
||||
var accessToken = GenerateAccessToken(stored.User, expires);
|
||||
|
||||
_db.ClinicalAuditLogs.Add(new ClinicalAuditLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Action = AuditAction.TokenRefreshed,
|
||||
EntityType = "ClinicalUser",
|
||||
EntityId = stored.UserId,
|
||||
UserId = stored.UserId,
|
||||
UserDisplayName = stored.User.DisplayName,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return new RefreshResponse(accessToken, newRefreshToken.Token, expires);
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(string refreshToken, Guid userId)
|
||||
{
|
||||
var stored = await _db.RefreshTokens
|
||||
.Include(t => t.User)
|
||||
.FirstOrDefaultAsync(t => t.Token == refreshToken && t.UserId == userId);
|
||||
|
||||
if (stored is not null && stored.RevokedAt is null)
|
||||
{
|
||||
stored.RevokedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
_db.ClinicalAuditLogs.Add(new ClinicalAuditLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Action = AuditAction.UserLogout,
|
||||
EntityType = "ClinicalUser",
|
||||
EntityId = userId,
|
||||
UserId = userId,
|
||||
UserDisplayName = stored.User.DisplayName,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<RefreshToken> CreateRefreshTokenAsync(Guid userId)
|
||||
{
|
||||
var token = new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Token = GenerateOpaqueToken(),
|
||||
UserId = userId,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(_jwt.RefreshTokenExpirationDays),
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
_db.RefreshTokens.Add(token);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
private static string GenerateOpaqueToken()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(64);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string GenerateAccessToken(ClinicalUser user, DateTimeOffset expires)
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user