Added refresh tokens and reorganized file structure
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
Reference in New Issue
Block a user