/
va1eriy
/
taskwards
Обзор
Документация
Войти
/
va1eriy
/
taskwards
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Taskwards.Api/Features/Auth/Endpoints/Refresh.cs
180 строк
6 KB
Valeriy Kokorev
Implement JWT refresh token rotation with attack detection
06 фев 2026, 13:50
06 фев 2026, 13:50
4199d8c
Код
Авторство
О чём код?
using FastEndpoints; using FluentValidation; using MediatR; using Microsoft.AspNetCore.Identity; using Taskwards.Api.Features.Auth; using Taskwards.Api.Features.Auth.Models; using Taskwards.Api.Infrastructure.Common; using Taskwards.Api.Infrastructure.Data.Entities; namespace Taskwards.Api.Features.Auth.Endpoints; /// <summary> /// Обновление токенов через refresh token (token rotation) /// </summary> public static class Refresh { // ===== FASTENDPOINTS ENDPOINT ===== public class Endpoint : Endpoint<Request, Response> { private readonly IMediator _mediator; public Endpoint(IMediator mediator) { _mediator = mediator; } public override void Configure() { Post("/auth/refresh"); AllowAnonymous(); Description(d => d .WithTags("Auth") .Produces<Response>(200) .ProducesProblemDetails(400) .ProducesProblemDetails(401)); } public override async Task HandleAsync(Request req, CancellationToken ct) { var command = new Command(req.RefreshToken); var result = await _mediator.Send(command, ct); if (!result.IsSuccess) { ThrowError(result.Error!, statusCode: 401); } var authResult = result.Value!; var response = new Response( authResult.UserId, authResult.Email, authResult.UserName, authResult.Tokens.AccessToken, authResult.Tokens.RefreshToken, authResult.Tokens.AccessTokenExpiration, authResult.Tokens.RefreshTokenExpiration ); await Send.OkAsync(response, ct); } } // ===== REQUEST ===== /// <summary> /// Запрос на обновление токенов /// </summary> public record Request( string RefreshToken ); // ===== RESPONSE ===== /// <summary> /// Новая пара токенов /// </summary> public record Response( Guid UserId, string Email, string UserName, string AccessToken, string RefreshToken, DateTime AccessTokenExpiration, DateTime RefreshTokenExpiration ); // ===== VALIDATOR ===== public class Validator : Validator<Request> { public Validator() { RuleFor(x => x.RefreshToken) .NotEmpty().WithMessage("Refresh token обязателен"); } } // ===== MEDIATR HANDLER ===== public record Command(string RefreshToken) : IRequest<Result<AuthResult>>; public class Handler : IRequestHandler<Command, Result<AuthResult>> { private readonly UserManager<User> _userManager; private readonly IJwtTokenGenerator _jwtTokenGenerator; private readonly IRefreshTokenService _refreshTokenService; public Handler( UserManager<User> userManager, IJwtTokenGenerator jwtTokenGenerator, IRefreshTokenService refreshTokenService) { _userManager = userManager; _jwtTokenGenerator = jwtTokenGenerator; _refreshTokenService = refreshTokenService; } public async Task<Result<AuthResult>> Handle(Command request, CancellationToken cancellationToken) { // 1. Валидация refresh token var validationResult = await _refreshTokenService.ValidateRefreshTokenAsync(request.RefreshToken, cancellationToken); if (!validationResult.IsValid) { // Если обнаружена атака, логируем (можно добавить logging) if (validationResult.IsSecurityBreach) { // TODO: Логирование попытки атаки } var errorMessage = validationResult.Error switch { RefreshTokenValidationError.TokenNotFound => "Недействительный refresh token", RefreshTokenValidationError.TokenExpired => "Refresh token истек", RefreshTokenValidationError.TokenRevoked => "Refresh token отозван", RefreshTokenValidationError.TokenRevokedAndReused => "Обнаружена попытка повторного использования отозванного токена", _ => "Ошибка валидации refresh token" }; return Result<AuthResult>.Failure(errorMessage); } var oldToken = validationResult.Token!; // 2. Получение пользователя var user = await _userManager.FindByIdAsync(oldToken.UserId.ToString()); if (user is null) { return Result<AuthResult>.Failure("Пользователь не найден"); } // 3. Проверка блокировки аккаунта if (await _userManager.IsLockedOutAsync(user)) { return Result<AuthResult>.Failure("Аккаунт заблокирован"); } // 4. Получение ролей var roles = await _userManager.GetRolesAsync(user); // 5. Генерация нового access token var accessToken = _jwtTokenGenerator.GenerateAccessToken(user, roles); // 6. Ротация refresh token (отзыв старого + создание нового в той же семье) var (newRefreshToken, _) = await _refreshTokenService.RotateRefreshTokenAsync(oldToken, cancellationToken); var tokenPair = new TokenPair( accessToken, newRefreshToken, _jwtTokenGenerator.GetAccessTokenExpiration(), _jwtTokenGenerator.GetRefreshTokenExpiration() ); var authResult = new AuthResult( user.Id, user.Email!, user.UserName!, tokenPair ); return Result<AuthResult>.Success(authResult); } } }