From c5823dc6fc3d8a30a4a6ffe1451c85af9d972886 Mon Sep 17 00:00:00 2001 From: Mateusz Skoczek Date: Thu, 5 Feb 2026 23:51:49 +0100 Subject: [PATCH] auth token endpoint added --- .../AuthPassword/AuthPasswordHandler.cs | 17 ++++--- .../Commands/AuthToken/AuthTokenCommand.cs | 10 +++++ .../Commands/AuthToken/AuthTokenHandler.cs | 39 ++++++++++++++++ .../Commands/AuthToken/AuthTokenResult.cs | 21 +++++++++ ...enGenerator.cs => AccessTokenGenerator.cs} | 44 ++++++++++++++----- ...nGenerator.cs => IAccessTokenGenerator.cs} | 6 +-- .../Program.cs | 2 +- .../WebAPI/Endpoints.cs | 17 ++++++- .../WebAPI/Mappers/AuthTokenMappers.cs | 13 ++++++ 9 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenCommand.cs create mode 100644 TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenHandler.cs create mode 100644 TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenResult.cs rename TimetableDesigner.Backend.Services.Authentication.Core/Helpers/{TokenGenerator.cs => AccessTokenGenerator.cs} (63%) rename TimetableDesigner.Backend.Services.Authentication.Core/Helpers/{ITokenGenerator.cs => IAccessTokenGenerator.cs} (56%) create mode 100644 TimetableDesigner.Backend.Services.Authentication/WebAPI/Mappers/AuthTokenMappers.cs diff --git a/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthPassword/AuthPasswordHandler.cs b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthPassword/AuthPasswordHandler.cs index 3b9e93e..fca35bd 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthPassword/AuthPasswordHandler.cs +++ b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthPassword/AuthPasswordHandler.cs @@ -10,13 +10,13 @@ public class AuthPasswordHandler : IRequestHandler Handle(AuthPasswordCommand request, CancellationToken cancellationToken) @@ -32,10 +32,13 @@ public class AuthPasswordHandler : IRequestHandler; \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenHandler.cs b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenHandler.cs new file mode 100644 index 0000000..619ff7f --- /dev/null +++ b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenHandler.cs @@ -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 +{ + private readonly DatabaseContext _databaseContext; + private readonly IAccessTokenGenerator _accessTokenGenerator; + + public AuthTokenHandler(DatabaseContext databaseContext, IAccessTokenGenerator accessTokenGenerator) + { + _databaseContext = databaseContext; + _accessTokenGenerator = accessTokenGenerator; + } + + public async Task 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); + } +} \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenResult.cs b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenResult.cs new file mode 100644 index 0000000..bb9f17c --- /dev/null +++ b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/AuthToken/AuthTokenResult.cs @@ -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); +} \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/TokenGenerator.cs b/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/AccessTokenGenerator.cs similarity index 63% rename from TimetableDesigner.Backend.Services.Authentication.Core/Helpers/TokenGenerator.cs rename to TimetableDesigner.Backend.Services.Authentication.Core/Helpers/AccessTokenGenerator.cs index 3ece1b7..3c79d14 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/TokenGenerator.cs +++ b/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/AccessTokenGenerator.cs @@ -9,12 +9,12 @@ using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegiste namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers; -public class TokenGenerator : ITokenGenerator +public class AccessTokenGenerator : IAccessTokenGenerator { private readonly IConfiguration _configuration; private readonly DatabaseContext _databaseContext; - public TokenGenerator(IConfiguration configuration, DatabaseContext databaseContext) + public AccessTokenGenerator(IConfiguration configuration, DatabaseContext databaseContext) { _configuration = configuration; _databaseContext = databaseContext; @@ -56,7 +56,7 @@ public class TokenGenerator : ITokenGenerator return handler.WriteToken(token); } - public async Task GenerateRefreshTokenAsync(Account account, bool isExtendable) + public RefreshToken GenerateRefreshToken(bool isExtendable) { string lifetimeSection = isExtendable ? "Extended" : "Normal"; int lifetime = _configuration.GetSection("Tokens") @@ -67,21 +67,41 @@ public class TokenGenerator : ITokenGenerator Guid guid = Guid.NewGuid(); DateTimeOffset expirationDate = DateTimeOffset.UtcNow.AddMinutes(lifetime); - RefreshToken refreshToken = new RefreshToken + return new RefreshToken { Token = guid, ExpirationDate = expirationDate, IsExtendable = isExtendable, - AccountId = account.Id, }; - await _databaseContext.RefreshTokens.AddAsync(refreshToken); - await _databaseContext.SaveChangesAsync(); - - return guid.ToString(); } - - public async Task ExtendRefreshTokenAsync() + + public bool ValidateExpiredAccessToken(string accessToken) { - return null; + IConfigurationSection accessTokenSettings = _configuration.GetSection("Tokens") + .GetSection("AccessToken"); + + string stringKey = accessTokenSettings.GetValue("Key")!; + byte[] encodedKey = Encoding.UTF8.GetBytes(stringKey); + SymmetricSecurityKey key = new SymmetricSecurityKey(encodedKey); + + string algorithm = accessTokenSettings.GetValue("Algorithm")!; + + TokenValidationParameters tokenValidation = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + ValidateAudience = true, + ValidateIssuer = true, + ValidateLifetime = false, + ValidIssuer = accessTokenSettings.GetValue("Issuer"), + ValidAudience = accessTokenSettings.GetValue("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); } } \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/ITokenGenerator.cs b/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/IAccessTokenGenerator.cs similarity index 56% rename from TimetableDesigner.Backend.Services.Authentication.Core/Helpers/ITokenGenerator.cs rename to TimetableDesigner.Backend.Services.Authentication.Core/Helpers/IAccessTokenGenerator.cs index b4b2dea..66fc148 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/ITokenGenerator.cs +++ b/TimetableDesigner.Backend.Services.Authentication.Core/Helpers/IAccessTokenGenerator.cs @@ -2,9 +2,9 @@ namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers; -public interface ITokenGenerator +public interface IAccessTokenGenerator { string GenerateAccessToken(Account account); - Task GenerateRefreshTokenAsync(Account account, bool isExtendable); - Task ExtendRefreshTokenAsync(); + RefreshToken GenerateRefreshToken(bool isExtendable); + bool ValidateExpiredAccessToken(string accessToken); } \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication/Program.cs b/TimetableDesigner.Backend.Services.Authentication/Program.cs index b1849f5..0f6185e 100644 --- a/TimetableDesigner.Backend.Services.Authentication/Program.cs +++ b/TimetableDesigner.Backend.Services.Authentication/Program.cs @@ -52,7 +52,7 @@ public static class Program private static IServiceCollection AddHelpers(this IServiceCollection services) { services.AddSingleton(); - services.AddScoped(); + services.AddScoped(); return services; } diff --git a/TimetableDesigner.Backend.Services.Authentication/WebAPI/Endpoints.cs b/TimetableDesigner.Backend.Services.Authentication/WebAPI/Endpoints.cs index 65ee374..2cef860 100644 --- a/TimetableDesigner.Backend.Services.Authentication/WebAPI/Endpoints.cs +++ b/TimetableDesigner.Backend.Services.Authentication/WebAPI/Endpoints.cs @@ -3,6 +3,7 @@ using FluentValidation.Results; using MediatR; using Microsoft.AspNetCore.Http.HttpResults; 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.DTO.WebAPI; using TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers; @@ -26,6 +27,10 @@ public static class Endpoints .Produces(500) .WithName("AuthPassword"); builder.MapPost("/auth/token", AuthToken) + .AllowAnonymous() + .Produces() + .Produces(401) + .Produces(500) .WithName("AuthToken"); return builder; @@ -58,8 +63,16 @@ public static class Endpoints return TypedResults.Ok(response); } - public static async Task, ProblemHttpResult>> AuthToken(AuthTokenRequest request) + private static async Task, 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); } } \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication/WebAPI/Mappers/AuthTokenMappers.cs b/TimetableDesigner.Backend.Services.Authentication/WebAPI/Mappers/AuthTokenMappers.cs new file mode 100644 index 0000000..7cc46d9 --- /dev/null +++ b/TimetableDesigner.Backend.Services.Authentication/WebAPI/Mappers/AuthTokenMappers.cs @@ -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!); +} \ No newline at end of file