Skip to content

Commit

Permalink
Refactor token handling and session management
Browse files Browse the repository at this point in the history
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
bvdcode committed Dec 17, 2024
1 parent db809f2 commit ad73ea6
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 55 deletions.
49 changes: 49 additions & 0 deletions Sources/Octockup.Server/Handlers/CreateTokenRequestHandler.cs
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());
}
}
}
41 changes: 7 additions & 34 deletions Sources/Octockup.Server/Handlers/LoginRequestHandler.cs
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 Sources/Octockup.Server/Handlers/RefreshRequestHandler.cs
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);
}
}
}
7 changes: 7 additions & 0 deletions Sources/Octockup.Server/Models/CreateTokenRequest.cs
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>;
}

0 comments on commit ad73ea6

Please sign in to comment.