Added refresh tokens and reorganized file structure

This commit is contained in:
Veljko Tosic
2026-02-14 18:48:39 +01:00
parent 888db766a3
commit 6b0a97e617
8 changed files with 152 additions and 6 deletions

View File

@@ -2,17 +2,21 @@ using AipsCore.Application.Common.Authentication;
using AipsCore.Domain.Common.Validation; using AipsCore.Domain.Common.Validation;
using AipsCore.Domain.Models.User.External; using AipsCore.Domain.Models.User.External;
using AipsCore.Domain.Models.User.Validation; using AipsCore.Domain.Models.User.Validation;
using AipsCore.Infrastructure.Persistence.Db;
using AipsCore.Infrastructure.Persistence.User.Mappers; using AipsCore.Infrastructure.Persistence.User.Mappers;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace AipsCore.Infrastructure.Persistence.Authentication; namespace AipsCore.Infrastructure.Persistence.Authentication.AuthService;
public class EfAuthService : IAuthService public class EfAuthService : IAuthService
{ {
private readonly AipsDbContext _dbContext;
private readonly UserManager<User.User> _userManager; private readonly UserManager<User.User> _userManager;
public EfAuthService(UserManager<User.User> userManager) public EfAuthService(AipsDbContext dbContext, UserManager<User.User> userManager)
{ {
_dbContext = dbContext;
_userManager = userManager; _userManager = userManager;
} }
@@ -61,4 +65,28 @@ public class EfAuthService : IAuthService
return new LoginResult(entity.MapToModel(), roles); return new LoginResult(entity.MapToModel(), roles);
} }
public async Task<LoginResult> LoginWithRefreshTokenAsync(Application.Common.Authentication.Models.RefreshToken refreshToken, CancellationToken cancellationToken = default)
{
var entity = await _userManager.FindByIdAsync(refreshToken.UserId);
if (entity is null)
{
throw new ValidationException(UserErrors.InvalidCredentials());
}
var roles = new List<UserRole>();
var rolesNames = await _userManager.GetRolesAsync(entity);
foreach (var roleName in rolesNames)
{
var role = UserRole.FromString(roleName);
if (role is null) throw new Exception($"Role {roleName} not found");
roles.Add(role);
}
return new LoginResult(entity.MapToModel(), roles);
}
} }

View File

@@ -1,4 +1,4 @@
namespace AipsCore.Infrastructure.Persistence.Authentication; namespace AipsCore.Infrastructure.Persistence.Authentication.Jwt;
public sealed class JwtSettings public sealed class JwtSettings
{ {
@@ -6,4 +6,5 @@ public sealed class JwtSettings
public string Audience { get; init; } = null!; public string Audience { get; init; } = null!;
public string Key { get; init; } = null!; public string Key { get; init; } = null!;
public int ExpirationMinutes { get; init; } public int ExpirationMinutes { get; init; }
public int RefreshTokenExpirationDays { get; init; }
} }

View File

@@ -1,11 +1,12 @@
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Text; using System.Text;
using AipsCore.Application.Abstract.UserContext; using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.User.External; using AipsCore.Domain.Models.User.External;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace AipsCore.Infrastructure.Persistence.Authentication; namespace AipsCore.Infrastructure.Persistence.Authentication.Jwt;
public class JwtTokenProvider : ITokenProvider public class JwtTokenProvider : ITokenProvider
{ {
@@ -16,7 +17,7 @@ public class JwtTokenProvider : ITokenProvider
_jwtSettings = jwtSettings; _jwtSettings = jwtSettings;
} }
public string Generate(Domain.Models.User.User user, IList<UserRole> roles) public string GenerateAccessToken(Domain.Models.User.User user, IList<UserRole> roles)
{ {
var claims = new List<Claim> var claims = new List<Claim>
{ {
@@ -42,4 +43,9 @@ public class JwtTokenProvider : ITokenProvider
return new JwtSecurityTokenHandler().WriteToken(token); return new JwtSecurityTokenHandler().WriteToken(token);
} }
public string GenerateRefreshToken()
{
return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
}
} }

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace AipsCore.Infrastructure.Persistence.Authentication.RefreshToken;
public class RefreshToken
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(255)]
public required string Token { get; set; }
public Guid UserId { get; set; }
public User.User User { get; set; } = null!;
public DateTime ExpiresAt { get; set; }
}

View File

@@ -0,0 +1,16 @@
namespace AipsCore.Infrastructure.Persistence.Authentication.RefreshToken;
public class RefreshTokenException : Exception
{
private const string InvalidTokenMessage = "Invalud token";
private const string TokenExpiredMessage = "Token expired";
public RefreshTokenException(string message)
: base(message)
{
}
public static RefreshTokenException InvalidToken() => new(InvalidTokenMessage);
public static RefreshTokenException TokenExpired() => new(TokenExpiredMessage);
}

View File

@@ -0,0 +1,12 @@
namespace AipsCore.Infrastructure.Persistence.Authentication.RefreshToken;
public static class RefreshTokenMappers
{
public static Application.Common.Authentication.Models.RefreshToken MapToModel(this RefreshToken entity)
{
return new Application.Common.Authentication.Models.RefreshToken(
entity.Token,
entity.UserId.ToString(),
entity.ExpiresAt);
}
}

View File

@@ -0,0 +1,64 @@
using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.User.ValueObjects;
using AipsCore.Infrastructure.Persistence.Authentication.Jwt;
using AipsCore.Infrastructure.Persistence.Db;
using Microsoft.EntityFrameworkCore;
namespace AipsCore.Infrastructure.Persistence.Authentication.RefreshToken;
public class RefreshTokenRepository : IRefreshTokenRepository
{
private readonly AipsDbContext _dbContext;
private readonly JwtSettings _jwtSettings;
public RefreshTokenRepository(AipsDbContext dbContext, JwtSettings jwtSettings)
{
_dbContext = dbContext;
_jwtSettings = jwtSettings;
}
public async Task AddAsync(string token, UserId userId, CancellationToken cancellationToken = default)
{
var refreshToken = new RefreshToken()
{
Id = Guid.NewGuid(),
Token = token,
UserId = new Guid(userId.IdValue),
ExpiresAt = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
};
await _dbContext.AddAsync(refreshToken, cancellationToken);
}
public async Task<Application.Common.Authentication.Models.RefreshToken> GetByValueAsync(string token, CancellationToken cancellationToken = default)
{
var entity = await _dbContext.RefreshTokens.FirstOrDefaultAsync(x => x.Token == token, cancellationToken);
if (entity is null)
{
throw RefreshTokenException.InvalidToken();
}
if (entity.ExpiresAt < DateTime.UtcNow)
{
throw RefreshTokenException.TokenExpired();
}
return entity.MapToModel();
}
public async Task RevokeAsync(string token, CancellationToken cancellationToken = default)
{
await _dbContext.RefreshTokens
.Where(x => x.Token == token)
.ExecuteDeleteAsync(cancellationToken);
}
public async Task RevokeAllAsync(UserId userId, CancellationToken cancellationToken = default)
{
await _dbContext.RefreshTokens
.Where(x => x.UserId == new Guid(userId.IdValue))
.ExecuteDeleteAsync(cancellationToken);
}
}

View File

@@ -3,7 +3,7 @@ using AipsCore.Application.Abstract.UserContext;
using AipsCore.Domain.Models.User.ValueObjects; using AipsCore.Domain.Models.User.ValueObjects;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace AipsCore.Infrastructure.Persistence.Authentication; namespace AipsCore.Infrastructure.Persistence.Authentication.UserContext;
public class HttpUserContext : IUserContext public class HttpUserContext : IUserContext
{ {