feat(infra): move authentication implementations and add ITokenGenerator
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public sealed record JwtOptions
|
||||
{
|
||||
public string Issuer { get; init; } = string.Empty;
|
||||
public string Audience { get; init; } = string.Empty;
|
||||
public string Secret { get; init; } = string.Empty;
|
||||
public int ExpiryInMinutes { get; init; } = 5;
|
||||
}
|
||||
|
||||
public sealed record JwtOptionsSetup : IConfigureOptions<JwtOptions>
|
||||
{
|
||||
private const string SectionName = nameof(JwtOptions);
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JwtOptionsSetup(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public void Configure(JwtOptions options)
|
||||
{
|
||||
_configuration.GetSection(SectionName).Bind(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public sealed class PasswordHasher : IPasswordHasher
|
||||
{
|
||||
private const int SaltSize = 16;
|
||||
private const int HashSize = 32;
|
||||
private const int Iterations = 100_000;
|
||||
|
||||
private static readonly HashAlgorithmName HashAlgorithm = HashAlgorithmName.SHA512;
|
||||
|
||||
private PasswordHasher()
|
||||
{
|
||||
}
|
||||
|
||||
public static PasswordHasher New()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public static PasswordHasher From(IServiceProvider serviceProvider)
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public string Hash(string password)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(SaltSize);
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
Iterations,
|
||||
HashAlgorithm,
|
||||
HashSize
|
||||
);
|
||||
|
||||
var hashBytes = new byte[SaltSize + HashSize];
|
||||
Array.Copy(salt, 0, hashBytes, 0, SaltSize);
|
||||
Array.Copy(hash, 0, hashBytes, SaltSize, HashSize);
|
||||
|
||||
return Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
public bool Verify(string providedPassword, string hashedPassword)
|
||||
{
|
||||
var hashBytes = Convert.FromBase64String(hashedPassword);
|
||||
|
||||
var salt = new byte[SaltSize];
|
||||
Array.Copy(hashBytes, 0, salt, 0, SaltSize);
|
||||
|
||||
var storedHash = new byte[HashSize];
|
||||
Array.Copy(hashBytes, SaltSize, storedHash, 0, HashSize);
|
||||
|
||||
var computedHash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
providedPassword,
|
||||
salt,
|
||||
Iterations,
|
||||
HashAlgorithm,
|
||||
HashSize
|
||||
);
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
namespace FiscalOS.Infra.Authentication;
|
||||
|
||||
public sealed class TokenGenerator : ITokenGenerator
|
||||
{
|
||||
private const int RefreshTokenExpiryInHours = 12;
|
||||
private readonly IOptions<JwtOptions> _jwtOptions;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private TokenGenerator(
|
||||
TimeProvider timeProvider,
|
||||
IOptions<JwtOptions> jwtOptions
|
||||
)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
_jwtOptions = jwtOptions;
|
||||
}
|
||||
|
||||
public static TokenGenerator From(IServiceProvider serviceProvider)
|
||||
{
|
||||
var timeProvider = serviceProvider.GetRequiredService<TimeProvider>();
|
||||
var jwtOptions = serviceProvider.GetRequiredService<IOptions<JwtOptions>>();
|
||||
return new(timeProvider, jwtOptions);
|
||||
}
|
||||
|
||||
public static TokenGenerator From(
|
||||
TimeProvider timeProvider,
|
||||
IOptions<JwtOptions> jwtOptions
|
||||
)
|
||||
{
|
||||
return new(timeProvider, jwtOptions);
|
||||
}
|
||||
|
||||
public string GenerateAccessToken(User user)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var secretKeyBytes = Encoding.UTF8.GetBytes(_jwtOptions.Value.Secret);
|
||||
var issuedAt = _timeProvider.GetUtcNow();
|
||||
var expiresAt = issuedAt.AddMinutes(_jwtOptions.Value.ExpiryInMinutes);
|
||||
List<Claim> claims = [
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
|
||||
];
|
||||
|
||||
var descriptor = new SecurityTokenDescriptor()
|
||||
{
|
||||
Subject = new(claims),
|
||||
IssuedAt = issuedAt.UtcDateTime,
|
||||
Expires = expiresAt.UtcDateTime,
|
||||
Issuer = _jwtOptions.Value.Issuer,
|
||||
Audience = _jwtOptions.Value.Audience,
|
||||
SigningCredentials = new(
|
||||
new SymmetricSecurityKey(secretKeyBytes),
|
||||
SecurityAlgorithms.HmacSha256Signature
|
||||
),
|
||||
};
|
||||
|
||||
var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);
|
||||
var jwtToken = tokenHandler.WriteToken(securityToken);
|
||||
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
public RefreshToken GenerateRefreshToken(User user)
|
||||
{
|
||||
var expiresAt = _timeProvider
|
||||
.GetUtcNow()
|
||||
.AddHours(RefreshTokenExpiryInHours);
|
||||
|
||||
return RefreshToken.From(user.Id, GenerateToken(), expiresAt);
|
||||
}
|
||||
|
||||
private static string GenerateToken()
|
||||
{
|
||||
var randomBytes = RandomNumberGenerator.GetBytes(32);
|
||||
var token = Convert.ToBase64String(randomBytes);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user