auth token endpoint added

This commit is contained in:
2026-02-05 23:51:49 +01:00
Unverified
parent 930f73d83d
commit c5823dc6fc
9 changed files with 144 additions and 25 deletions

View File

@@ -10,13 +10,13 @@ public class AuthPasswordHandler : IRequestHandler<AuthPasswordCommand, AuthPass
{ {
private readonly DatabaseContext _databaseContext; private readonly DatabaseContext _databaseContext;
private readonly IPasswordHasher _passwordHasher; private readonly IPasswordHasher _passwordHasher;
private readonly ITokenGenerator _tokenGenerator; private readonly IAccessTokenGenerator _accessTokenGenerator;
public AuthPasswordHandler(DatabaseContext databaseContext, IPasswordHasher passwordHasher, ITokenGenerator tokenGenerator) public AuthPasswordHandler(DatabaseContext databaseContext, IPasswordHasher passwordHasher, IAccessTokenGenerator accessTokenGenerator)
{ {
_databaseContext = databaseContext; _databaseContext = databaseContext;
_passwordHasher = passwordHasher; _passwordHasher = passwordHasher;
_tokenGenerator = tokenGenerator; _accessTokenGenerator = accessTokenGenerator;
} }
public async Task<AuthPasswordResult> Handle(AuthPasswordCommand request, CancellationToken cancellationToken) public async Task<AuthPasswordResult> Handle(AuthPasswordCommand request, CancellationToken cancellationToken)
@@ -32,10 +32,13 @@ public class AuthPasswordHandler : IRequestHandler<AuthPasswordCommand, AuthPass
{ {
return AuthPasswordResult.Failure(); return AuthPasswordResult.Failure();
} }
string accessToken = _accessTokenGenerator.GenerateAccessToken(account);
RefreshToken refreshToken = _accessTokenGenerator.GenerateRefreshToken(request.RememberMe);
account.RefreshTokens.Add(refreshToken);
await _databaseContext.SaveChangesAsync(cancellationToken);
string refreshToken = await _tokenGenerator.GenerateRefreshTokenAsync(account, request.RememberMe); return AuthPasswordResult.Success(accessToken, refreshToken.Token.ToString());
string accessToken = _tokenGenerator.GenerateAccessToken(account);
return AuthPasswordResult.Success(refreshToken, accessToken);
} }
} }

View File

@@ -0,0 +1,10 @@
using MediatR;
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthToken;
public record AuthTokenCommand
(
string AccessToken,
string RefreshToken
)
: IRequest<AuthTokenResult>;

View File

@@ -0,0 +1,39 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
using TimetableDesigner.Backend.Services.Authentication.Database;
using TimetableDesigner.Backend.Services.Authentication.Database.Model;
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthToken;
public class AuthTokenHandler : IRequestHandler<AuthTokenCommand, AuthTokenResult>
{
private readonly DatabaseContext _databaseContext;
private readonly IAccessTokenGenerator _accessTokenGenerator;
public AuthTokenHandler(DatabaseContext databaseContext, IAccessTokenGenerator accessTokenGenerator)
{
_databaseContext = databaseContext;
_accessTokenGenerator = accessTokenGenerator;
}
public async Task<AuthTokenResult> Handle(AuthTokenCommand request, CancellationToken cancellationToken)
{
RefreshToken? token = await _databaseContext.RefreshTokens
.Include(x => x.Account)
.FirstOrDefaultAsync(x => x.Token == Guid.Parse(request.RefreshToken), cancellationToken);
if (token is null || token.ExpirationDate < DateTimeOffset.UtcNow || !_accessTokenGenerator.ValidateExpiredAccessToken(request.AccessToken))
{
return AuthTokenResult.Failure();
}
string accessToken = _accessTokenGenerator.GenerateAccessToken(token.Account);
if (token.IsExtendable)
{
}
return AuthTokenResult.Success(refreshToken, accessToken);
}
}

View File

@@ -0,0 +1,21 @@
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthToken;
public record AuthTokenResult
{
public bool IsSuccess { get; }
public string? AccessToken { get; }
public string? RefreshToken { get; }
private AuthTokenResult(bool isSuccess, string? accessToken, string? refreshToken)
{
IsSuccess = isSuccess;
AccessToken = accessToken;
RefreshToken = refreshToken;
}
public static AuthTokenResult Success(string accessToken, string refreshToken) =>
new AuthTokenResult(true, accessToken, refreshToken);
public static AuthTokenResult Failure() =>
new AuthTokenResult(false, null, null);
}

View File

@@ -9,12 +9,12 @@ using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegiste
namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers; namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
public class TokenGenerator : ITokenGenerator public class AccessTokenGenerator : IAccessTokenGenerator
{ {
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly DatabaseContext _databaseContext; private readonly DatabaseContext _databaseContext;
public TokenGenerator(IConfiguration configuration, DatabaseContext databaseContext) public AccessTokenGenerator(IConfiguration configuration, DatabaseContext databaseContext)
{ {
_configuration = configuration; _configuration = configuration;
_databaseContext = databaseContext; _databaseContext = databaseContext;
@@ -56,7 +56,7 @@ public class TokenGenerator : ITokenGenerator
return handler.WriteToken(token); return handler.WriteToken(token);
} }
public async Task<string> GenerateRefreshTokenAsync(Account account, bool isExtendable) public RefreshToken GenerateRefreshToken(bool isExtendable)
{ {
string lifetimeSection = isExtendable ? "Extended" : "Normal"; string lifetimeSection = isExtendable ? "Extended" : "Normal";
int lifetime = _configuration.GetSection("Tokens") int lifetime = _configuration.GetSection("Tokens")
@@ -67,21 +67,41 @@ public class TokenGenerator : ITokenGenerator
Guid guid = Guid.NewGuid(); Guid guid = Guid.NewGuid();
DateTimeOffset expirationDate = DateTimeOffset.UtcNow.AddMinutes(lifetime); DateTimeOffset expirationDate = DateTimeOffset.UtcNow.AddMinutes(lifetime);
RefreshToken refreshToken = new RefreshToken return new RefreshToken
{ {
Token = guid, Token = guid,
ExpirationDate = expirationDate, ExpirationDate = expirationDate,
IsExtendable = isExtendable, IsExtendable = isExtendable,
AccountId = account.Id,
}; };
await _databaseContext.RefreshTokens.AddAsync(refreshToken);
await _databaseContext.SaveChangesAsync();
return guid.ToString();
} }
public async Task<string> ExtendRefreshTokenAsync() public bool ValidateExpiredAccessToken(string accessToken)
{ {
return null; IConfigurationSection accessTokenSettings = _configuration.GetSection("Tokens")
.GetSection("AccessToken");
string stringKey = accessTokenSettings.GetValue<string>("Key")!;
byte[] encodedKey = Encoding.UTF8.GetBytes(stringKey);
SymmetricSecurityKey key = new SymmetricSecurityKey(encodedKey);
string algorithm = accessTokenSettings.GetValue<string>("Algorithm")!;
TokenValidationParameters tokenValidation = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = false,
ValidIssuer = accessTokenSettings.GetValue<string>("Issuer"),
ValidAudience = accessTokenSettings.GetValue<string>("Audience"),
IssuerSigningKey = key,
ClockSkew = TimeSpan.FromMinutes(1),
};
JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(accessToken, tokenValidation, out SecurityToken validatedToken);
JwtSecurityToken? jwtSecurityToken = validatedToken as JwtSecurityToken;
return jwtSecurityToken is not null && jwtSecurityToken.Header.Alg.Equals(algorithm, StringComparison.InvariantCultureIgnoreCase);
} }
} }

View File

@@ -2,9 +2,9 @@
namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers; namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
public interface ITokenGenerator public interface IAccessTokenGenerator
{ {
string GenerateAccessToken(Account account); string GenerateAccessToken(Account account);
Task<string> GenerateRefreshTokenAsync(Account account, bool isExtendable); RefreshToken GenerateRefreshToken(bool isExtendable);
Task<string> ExtendRefreshTokenAsync(); bool ValidateExpiredAccessToken(string accessToken);
} }

View File

@@ -52,7 +52,7 @@ public static class Program
private static IServiceCollection AddHelpers(this IServiceCollection services) private static IServiceCollection AddHelpers(this IServiceCollection services)
{ {
services.AddSingleton<IPasswordHasher, PasswordHasher>(); services.AddSingleton<IPasswordHasher, PasswordHasher>();
services.AddScoped<ITokenGenerator, TokenGenerator>(); services.AddScoped<IAccessTokenGenerator, AccessTokenGenerator>();
return services; return services;
} }

View File

@@ -3,6 +3,7 @@ using FluentValidation.Results;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword; using TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword;
using TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthToken;
using TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register; using TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register;
using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI; using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
using TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers; using TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers;
@@ -26,6 +27,10 @@ public static class Endpoints
.Produces(500) .Produces(500)
.WithName("AuthPassword"); .WithName("AuthPassword");
builder.MapPost("/auth/token", AuthToken) builder.MapPost("/auth/token", AuthToken)
.AllowAnonymous()
.Produces<AuthResponse>()
.Produces(401)
.Produces(500)
.WithName("AuthToken"); .WithName("AuthToken");
return builder; return builder;
@@ -58,8 +63,16 @@ public static class Endpoints
return TypedResults.Ok(response); return TypedResults.Ok(response);
} }
public static async Task<Results<Ok<AuthResponse>, ProblemHttpResult>> AuthToken(AuthTokenRequest request) private static async Task<Results<Ok<AuthResponse>, UnauthorizedHttpResult, InternalServerError>> AuthToken(IMediator mediator, AuthTokenRequest request, CancellationToken cancellationToken)
{ {
return null; AuthTokenResult result = await mediator.Send(request.ToCommand(), cancellationToken);
if (!result.IsSuccess)
{
return TypedResults.Unauthorized();
}
AuthResponse response = result.ToResponse();
return TypedResults.Ok(response);
} }
} }

View File

@@ -0,0 +1,13 @@
using TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthToken;
using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
namespace TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers;
public static class AuthTokenMappers
{
public static AuthTokenCommand ToCommand(this AuthTokenRequest request) =>
new AuthTokenCommand(request.AccessToken, request.RefreshToken);
public static AuthResponse ToResponse(this AuthTokenResult result) =>
new AuthResponse(result.AccessToken!, result.RefreshToken!);
}