using Socialize.Infrastructure.Security; using Socialize.Modules.Identity.Configuration; using Socialize.Modules.Identity.Data; using Socialize.Modules.Identity.Services; using Microsoft.Extensions.Options; namespace Socialize.Modules.Identity.Handlers; [PublicAPI] public record RefreshTokenRequest( string RefreshToken); [PublicAPI] public record RefreshTokenResponse( string AccessToken, string RefreshToken); [PublicAPI] public class RefreshTokenHandler( UserManager userManager, IOptionsSnapshot jwtOptions, AccessTokenFactory accessTokenFactory) : Endpoint { public override void Configure() { AllowAnonymous(); Post("/api/users/refresh"); Options(o => o.WithTags("Users")); } public override async Task HandleAsync( RefreshTokenRequest request, CancellationToken ct) { // Find the user using the refresh token User? user = await userManager.Users .FirstOrDefaultAsync(u => u.RefreshToken == request.RefreshToken, ct); if (user == null || user.RefreshTokenExpiryTime <= DateTime.UtcNow) { await SendUnauthorizedAsync(ct); return; } // Generate a new refresh token if rotation is required if (jwtOptions.Value.RefreshTokenRequireRotation || user.RefreshToken is null) { user.RefreshToken = RefreshTokenGenerator.Next(); } // Update refresh token expiry time user.RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime); await userManager.UpdateAsync(user); // Generate a new access token string accessToken = await accessTokenFactory.CreateAsync(user); await SendOkAsync( new RefreshTokenResponse(accessToken, user.RefreshToken), ct); } }