/
va1eriy
/
taskwards
Обзор
Документация
Войти
/
va1eriy
/
taskwards
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Taskwards.Api/Features/Auth/RefreshTokenService.cs
167 строк
6 KB
Valeriy Kokorev
feat: implement password reset functionality
08 фев 2026, 19:09
08 фев 2026, 19:09
7ee9481
Код
Авторство
О чём код?
using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; using Taskwards.Api.Features.Auth.Models; using Taskwards.Api.Infrastructure.Data; using Taskwards.Api.Infrastructure.Data.Entities; namespace Taskwards.Api.Features.Auth; public class RefreshTokenService : IRefreshTokenService { private readonly TaskwardsDbContext _context; private readonly IJwtTokenGenerator _jwtTokenGenerator; public RefreshTokenService(TaskwardsDbContext context, IJwtTokenGenerator jwtTokenGenerator) { _context = context; _jwtTokenGenerator = jwtTokenGenerator; } public async Task<(string rawToken, RefreshToken entity)> CreateRefreshTokenAsync( Guid userId, Guid tokenFamily, CancellationToken cancellationToken = default) { var rawToken = _jwtTokenGenerator.GenerateRefreshToken(); var tokenHash = HashToken(rawToken); var refreshToken = new RefreshToken { TokenHash = tokenHash, UserId = userId, TokenFamily = tokenFamily, ExpiresAt = _jwtTokenGenerator.GetRefreshTokenExpiration(), CreatedAt = DateTime.UtcNow, RevokedAt = null, ReplacedByTokenHash = null, RevokedReason = null }; _context.RefreshTokens.Add(refreshToken); await _context.SaveChangesAsync(cancellationToken); return (rawToken, refreshToken); } public async Task<RefreshTokenValidationResult> ValidateRefreshTokenAsync(string rawToken, CancellationToken cancellationToken = default) { var tokenHash = HashToken(rawToken); var token = await _context.RefreshTokens .FirstOrDefaultAsync(rt => rt.TokenHash == tokenHash, cancellationToken); if (token == null) { return RefreshTokenValidationResult.Failure(RefreshTokenValidationError.TokenNotFound); } // Проверка на повторное использование отозванного токена if (token.IsRevoked) { // Если токен был отозван из-за ротации - это атака (старый токен используется вместо нового) if (token.RevokedReason == "Rotated") { // Отозванный токен после ротации используется повторно - возможная кража токена. // Отзываем всю семью токенов await RevokeTokenFamilyAsync(token.TokenFamily, "SecurityBreach: Revoked token reuse detected", cancellationToken); return RefreshTokenValidationResult.Failure(RefreshTokenValidationError.TokenRevokedAndReused, isSecurityBreach: true); } // Токен был отозван по другим причинам (logout, manual revoke) - просто возвращаем ошибку return RefreshTokenValidationResult.Failure(RefreshTokenValidationError.TokenRevoked); } if (token.IsExpired) { return RefreshTokenValidationResult.Failure(RefreshTokenValidationError.TokenExpired); } return RefreshTokenValidationResult.Success(token); } public async Task<(string rawToken, RefreshToken entity)> RotateRefreshTokenAsync( RefreshToken oldToken, CancellationToken cancellationToken = default) { // Создаем новый токен в той же семье var (newRawToken, newToken) = await CreateRefreshTokenAsync(oldToken.UserId, oldToken.TokenFamily, cancellationToken); // Отзываем старый токен и указываем замену oldToken.RevokedAt = DateTime.UtcNow; oldToken.RevokedReason = "Rotated"; oldToken.ReplacedByTokenHash = newToken.TokenHash; await _context.SaveChangesAsync(cancellationToken); return (newRawToken, newToken); } public async Task RevokeRefreshTokenAsync(string rawToken, string reason, CancellationToken cancellationToken = default) { var tokenHash = HashToken(rawToken); var token = await _context.RefreshTokens .FirstOrDefaultAsync(rt => rt.TokenHash == tokenHash, cancellationToken); if (token == null || token.IsRevoked) { return; // Токен не найден или уже отозван } token.RevokedAt = DateTime.UtcNow; token.RevokedReason = reason; await _context.SaveChangesAsync(cancellationToken); } public async Task RevokeTokenFamilyAsync(Guid tokenFamily, string reason, CancellationToken cancellationToken = default) { var tokensInFamily = await _context.RefreshTokens .Where(rt => rt.TokenFamily == tokenFamily && rt.RevokedAt == null) .ToListAsync(cancellationToken); foreach (var token in tokensInFamily) { token.RevokedAt = DateTime.UtcNow; token.RevokedReason = reason; } await _context.SaveChangesAsync(cancellationToken); } public async Task RevokeAllUserTokensAsync(Guid userId, string reason, CancellationToken cancellationToken = default) { var userTokens = await _context.RefreshTokens .Where(rt => rt.UserId == userId && rt.RevokedAt == null) .ToListAsync(cancellationToken); foreach (var token in userTokens) { token.RevokedAt = DateTime.UtcNow; token.RevokedReason = reason; } await _context.SaveChangesAsync(cancellationToken); } public async Task CleanupExpiredTokensAsync(CancellationToken cancellationToken = default) { var cutoffDate = DateTime.UtcNow.AddDays(-30); // Удаляем токены старше 30 дней var expiredTokens = await _context.RefreshTokens .Where(rt => rt.ExpiresAt < DateTime.UtcNow || (rt.RevokedAt != null && rt.RevokedAt < cutoffDate)) .ToListAsync(cancellationToken); _context.RefreshTokens.RemoveRange(expiredTokens); await _context.SaveChangesAsync(cancellationToken); } private static string HashToken(string token) { var bytes = Encoding.UTF8.GetBytes(token); var hash = SHA256.HashData(bytes); return Convert.ToHexString(hash); } }