-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor token handling and session management
Refactored LoginRequestHandler and RefreshRequestHandler to use IMediator for token creation and session management. Introduced CreateTokenRequestHandler for handling token creation and user sessions. Added CreateTokenRequest record for encapsulating user info. Streamlined WebApiException usage. Moved GetUserClaims method to CreateTokenRequestHandler. Updated RefreshRequestHandler to throw WebApiException for unauthorized and not found cases. Ensured refresh token lifetime validation in CreateTokenRequestHandler. Mapped user entity to UserDto in AuthResponse.
- Loading branch information
Showing
4 changed files
with
75 additions
and
55 deletions.
There are no files selected for viewing
49 changes: 49 additions & 0 deletions
49
Sources/Octockup.Server/Handlers/CreateTokenRequestHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using MediatR; | ||
using AutoMapper; | ||
using System.Security.Claims; | ||
using Octockup.Server.Models; | ||
using Octockup.Server.Database; | ||
using Octockup.Server.Models.Dto; | ||
using EasyExtensions.AspNetCore.Authorization.Builders; | ||
using EasyExtensions.AspNetCore.Authorization.Services; | ||
|
||
namespace Octockup.Server.Handlers | ||
{ | ||
|
||
public class CreateTokenRequestHandler(ITokenProvider _tokenProvider, AppDbContext _dbContext, | ||
IConfiguration _configuration, IMapper _mapper) : IRequestHandler<CreateTokenRequest, AuthResponse> | ||
{ | ||
public async Task<AuthResponse> Handle(CreateTokenRequest request, CancellationToken cancellationToken) | ||
{ | ||
string accessToken = _tokenProvider.CreateToken(x => GetUserClaims(x, request.User)); | ||
int refreshLifetime = _configuration.GetValue<int>("JwtSettings:RefreshLifetimeHours"); | ||
if (refreshLifetime <= 0) | ||
{ | ||
throw new InvalidOperationException("JwtSettings:RefreshLifetimeHours must be greater than 0"); | ||
} | ||
TimeSpan refreshLifetimeSpan = TimeSpan.FromHours(refreshLifetime); | ||
Session session = new() | ||
{ | ||
User = request.User, | ||
UserId = request.User.Id, | ||
RefreshToken = _tokenProvider.CreateToken(refreshLifetimeSpan, x => GetUserClaims(x, request.User)), | ||
}; | ||
_dbContext.Sessions.Add(session); | ||
await _dbContext.SaveChangesAsync(cancellationToken); | ||
UserDto userDto = _mapper.Map<UserDto>(request.User); | ||
return new AuthResponse(accessToken, session.RefreshToken, userDto); | ||
} | ||
|
||
private static ClaimBuilder GetUserClaims(ClaimBuilder builder, User user) | ||
{ | ||
ArgumentNullException.ThrowIfNull(user, nameof(user)); | ||
ArgumentException.ThrowIfNullOrWhiteSpace(user.Username, nameof(user.Username)); | ||
ArgumentException.ThrowIfNullOrWhiteSpace(user.Email, nameof(user.Email)); | ||
|
||
return builder.Add(ClaimTypes.Name, user.Username) | ||
.Add(ClaimTypes.Role, user.Role.ToString()) | ||
.Add(ClaimTypes.Email, user.Email) | ||
.Add(ClaimTypes.Sid, user.Id.ToString()); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,26 @@ | ||
using MediatR; | ||
using AutoMapper; | ||
using System.Net; | ||
using Octockup.Server.Models; | ||
using System.Security.Claims; | ||
using Octockup.Server.Database; | ||
using Octockup.Server.Models.Dto; | ||
using EasyExtensions.EntityFrameworkCore.Exceptions; | ||
using EasyExtensions.AspNetCore.Authorization.Services; | ||
using EasyExtensions.AspNetCore.Authorization.Builders; | ||
|
||
namespace Octockup.Server.Handlers | ||
{ | ||
public class LoginRequestHandler(ILogger<LoginRequestHandler> _logger, ITokenProvider _tokenProvider, | ||
AppDbContext _dbContext, IConfiguration _configuration, IMapper _mapper) : IRequestHandler<LoginRequest, AuthResponse> | ||
public class LoginRequestHandler(ILogger<LoginRequestHandler> _logger, AppDbContext _dbContext, | ||
IMediator _mediator) : IRequestHandler<LoginRequest, AuthResponse> | ||
{ | ||
public async Task<AuthResponse> Handle(LoginRequest request, CancellationToken cancellationToken) | ||
{ | ||
var foundUser = _dbContext.Users.FirstOrDefault(x => x.Username.Equals(request.Username, StringComparison.CurrentCultureIgnoreCase)) | ||
?? throw new WebApiException(System.Net.HttpStatusCode.NotFound, nameof(User), "User not found"); | ||
?? throw new WebApiException(HttpStatusCode.NotFound, nameof(User), "User not found"); | ||
if (!foundUser.PasswordHash.Equals(request.PasswordHash)) | ||
{ | ||
_logger.LogWarning("Login attempt for '{Username}' failed", request.Username); | ||
throw new WebApiException(System.Net.HttpStatusCode.Unauthorized, nameof(User), "Invalid password"); | ||
throw new WebApiException(HttpStatusCode.Unauthorized, nameof(User), "Invalid password"); | ||
} | ||
_logger.LogInformation("User '{Username}' logged in", request.Username); | ||
string accessToken = _tokenProvider.CreateToken(x => GetUserClaims(x, foundUser)); | ||
int refreshLifetime = _configuration.GetValue<int>("JwtSettings:RefreshLifetimeHours"); | ||
if (refreshLifetime <= 0) | ||
{ | ||
throw new InvalidOperationException("JwtSettings:RefreshLifetimeHours must be greater than 0"); | ||
} | ||
TimeSpan refreshLifetimeSpan = TimeSpan.FromHours(refreshLifetime); | ||
Session session = new() | ||
{ | ||
User = foundUser, | ||
UserId = foundUser.Id, | ||
RefreshToken = _tokenProvider.CreateToken(refreshLifetimeSpan, x => GetUserClaims(x, foundUser)), | ||
}; | ||
_dbContext.Sessions.Add(session); | ||
await _dbContext.SaveChangesAsync(cancellationToken); | ||
UserDto userDto = _mapper.Map<UserDto>(foundUser); | ||
return new(accessToken, session.RefreshToken, userDto); | ||
} | ||
|
||
private static ClaimBuilder GetUserClaims(ClaimBuilder builder, User user) | ||
{ | ||
return builder.Add(ClaimTypes.Name, user.Username) | ||
.Add(ClaimTypes.Role, user.Role.ToString()) | ||
.Add(ClaimTypes.Email, user.Email) | ||
.Add(ClaimTypes.Sid, user.Id.ToString()); | ||
CreateTokenRequest createTokenRequest = new(foundUser); | ||
return await _mediator.Send(createTokenRequest, cancellationToken); | ||
} | ||
} | ||
} |
33 changes: 12 additions & 21 deletions
33
Sources/Octockup.Server/Handlers/RefreshRequestHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,28 @@ | ||
using MediatR; | ||
using AutoMapper; | ||
using Octockup.Server.Models; | ||
using Octockup.Server.Database; | ||
using EasyExtensions.EntityFrameworkCore.Exceptions; | ||
using EasyExtensions.AspNetCore.Authorization.Services; | ||
|
||
namespace Octockup.Server.Handlers | ||
{ | ||
public class RefreshRequestHandler : IRequestHandler<RefreshRequest, AuthResponse> | ||
public class RefreshRequestHandler(ITokenProvider _tokenProvider, IMediator _mediator, | ||
AppDbContext _dbContext) : IRequestHandler<RefreshRequest, AuthResponse> | ||
{ | ||
public Task<AuthResponse> Handle(RefreshRequest request, CancellationToken cancellationToken) | ||
public async Task<AuthResponse> Handle(RefreshRequest request, CancellationToken cancellationToken) | ||
{ | ||
bool isValid = _tokenProvider.ValidateToken(request.RefreshToken); | ||
if (!isValid) | ||
{ | ||
|
||
return Unauthorized(); | ||
} | ||
var foundToken = _dbContext.Sessions.FirstOrDefault(x => x.RefreshToken == request.RefreshToken); | ||
if (foundToken is null) | ||
{ | ||
return NotFound(); | ||
throw new WebApiException(System.Net.HttpStatusCode.Unauthorized, nameof(Session), "Invalid refresh token"); | ||
} | ||
var foundToken = _dbContext.Sessions.FirstOrDefault(x => x.RefreshToken == request.RefreshToken) | ||
?? throw new WebApiException(System.Net.HttpStatusCode.NotFound, nameof(Session), "Session not found"); | ||
_dbContext.Sessions.Remove(foundToken); | ||
await _dbContext.SaveChangesAsync(); | ||
// JwtSettingsRefreshLifetimeHours | ||
int hours = 720; | ||
string token = _tokenProvider.CreateToken(TimeSpan.FromHours(hours)); | ||
Session session = new() | ||
{ | ||
UserId = foundToken.UserId, | ||
RefreshToken = _tokenProvider.CreateToken(x => x.Add(ClaimTypes.Name, "refresh")) | ||
}; | ||
_dbContext.Sessions.Add(session); | ||
await _dbContext.SaveChangesAsync(); | ||
return Ok(new AuthResponse(token, session.RefreshToken)); | ||
await _dbContext.SaveChangesAsync(cancellationToken); | ||
CreateTokenRequest createTokenRequest = new(foundToken.User); | ||
return await _mediator.Send(createTokenRequest, cancellationToken); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
using MediatR; | ||
using Octockup.Server.Database; | ||
|
||
namespace Octockup.Server.Models | ||
{ | ||
public record CreateTokenRequest(User User) : IRequest<AuthResponse>; | ||
} |