auth token endpoint added
This commit is contained in:
@@ -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)
|
||||||
@@ -33,9 +33,12 @@ public class AuthPasswordHandler : IRequestHandler<AuthPasswordCommand, AuthPass
|
|||||||
return AuthPasswordResult.Failure();
|
return AuthPasswordResult.Failure();
|
||||||
}
|
}
|
||||||
|
|
||||||
string refreshToken = await _tokenGenerator.GenerateRefreshTokenAsync(account, request.RememberMe);
|
string accessToken = _accessTokenGenerator.GenerateAccessToken(account);
|
||||||
string accessToken = _tokenGenerator.GenerateAccessToken(account);
|
RefreshToken refreshToken = _accessTokenGenerator.GenerateRefreshToken(request.RememberMe);
|
||||||
|
|
||||||
return AuthPasswordResult.Success(refreshToken, accessToken);
|
account.RefreshTokens.Add(refreshToken);
|
||||||
|
await _databaseContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return AuthPasswordResult.Success(accessToken, refreshToken.Token.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthToken;
|
||||||
|
|
||||||
|
public record AuthTokenCommand
|
||||||
|
(
|
||||||
|
string AccessToken,
|
||||||
|
string RefreshToken
|
||||||
|
)
|
||||||
|
: IRequest<AuthTokenResult>;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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!);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user