feat(infra): move authentication implementations and add ITokenGenerator

This commit is contained in:
Stevan Freeborn
2026-02-03 04:38:40 -06:00
parent c4d0bf78ab
commit a1b993bd62
6 changed files with 50 additions and 37 deletions
@@ -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;
}
}