//
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
//
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Text;
using Astral.Core.Constants;
using Astral.Core.Entities;
using Astral.Core.Extensions;
using Astral.Core.Infrastructure;
using Astral.Core.Options;
using Astral.Core.RepositoryInterfaces;
using Astral.Services.Constants;
using Astral.Services.Dtos;
using Astral.Services.Exceptions;
using Astral.Services.Interfaces;
using FluentValidation;
using Injectio.Attributes;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace Astral.Services.Services;
///
[RegisterScoped]
public class AuthenticationService : IAuthenticationService
{
private const int RefreshTokenLength = 128;
private readonly ICryptographyService _cryptographyService;
private readonly ITransactionProvider _transactionProvider;
private readonly IUserRepository _userRepository;
private readonly IRefreshTokenRepository _refreshTokenRepository;
private readonly IValidator _passwordAuthValidator;
private readonly JwtOptions _jwtOptions;
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
/// Instance of .
/// Instance of .
/// Instance of .
/// Instance of .
/// Instance of .
/// Instance of .
/// Instance of .
public AuthenticationService(
ICryptographyService cryptographyService,
ITransactionProvider transactionProvider,
IUserRepository userRepository,
IRefreshTokenRepository refreshTokenRepository,
IValidator passwordAuthValidator,
IOptions jwtOptions,
ILogger logger)
{
_cryptographyService = cryptographyService;
_transactionProvider = transactionProvider;
_userRepository = userRepository;
_refreshTokenRepository = refreshTokenRepository;
_passwordAuthValidator = passwordAuthValidator;
_jwtOptions = jwtOptions.Value;
_logger = logger;
}
///
public async Task AuthenticateSession(PasswordAuthenticateDto passwordAuthenticateDto)
{
await _passwordAuthValidator.ValidateAndThrowAsync(passwordAuthenticateDto);
// Find user from provided username.
var user = await _userRepository.FindByUsername(passwordAuthenticateDto.Username);
if (user is null)
{
throw new InvalidCredentialsException();
}
if (!_cryptographyService.VerifyPassword(
passwordAuthenticateDto.Password,
user.PasswordSalt,
user.PasswordHash))
{
// TODO store failed logins in future
throw new InvalidCredentialsException();
}
switch (user.State)
{
case UserState.Suspended:
throw new UserSuspendedException();
case UserState.Banned:
throw new UserBannedException();
case UserState.AwaitingEmailActivation:
throw new UserNotActivatedException();
}
using var transaction = await _transactionProvider.BeginTransactionAsync();
// Record last logged in.
// TODO store successful login in future
user.LastLoggedIn = DateTime.UtcNow;
await _userRepository.UpdateAsync(user);
// Generate token.
var result = await GenerateAccessTokens(user, passwordAuthenticateDto.Scope, passwordAuthenticateDto.IpAddress);
_logger.LogInformation("Granted authorisation to user {user}", user.Username);
transaction.Commit();
return result;
}
///
/// Generate a new access token and refresh token for user.
///
/// Instance of .
/// Instance of .
/// The IP address of the agent requesting a user access token.
/// Instance of .
private async Task GenerateAccessTokens(
User user,
TokenScope scope,
IPAddress ipAddress)
{
var handler = new JwtSecurityTokenHandler();
var claims = new List
{
new (ClaimIds.UserId, user.Id.ToString()),
new (ClaimIds.UserName, user.Username),
new (ClaimIds.Role, user.Role.ToString()),
new (ClaimIds.Scope, scope.ToString()),
};
var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_jwtOptions.SecretKey));
var accessExpires = _jwtOptions.AccessTokenExpirationDateTime();
var refreshExpires = _jwtOptions.RefreshTokenExpirationDateTime();
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = accessExpires,
SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature),
Audience = _jwtOptions.Audience,
Issuer = _jwtOptions.Issuer,
};
var result = new SessionDto();
var token = handler.CreateToken(descriptor);
result.UserId = user.Id;
result.UserName = user.Username;
result.CreatedAt = DateTime.UtcNow;
result.AccessToken = handler.WriteToken(token);
result.RefreshToken = _cryptographyService.GenerateRandomString(RefreshTokenLength);
result.AccessTokenExpires = accessExpires;
result.RefreshTokenExpires = refreshExpires;
result.Scope = scope;
result.Role = user.Role;
// Make sure the refresh token is unique.
while (await _refreshTokenRepository.FindByIdAsync(result.RefreshToken) is not null)
{
result.RefreshToken = _cryptographyService.GenerateRandomString(RefreshTokenLength);
}
// Store refresh token.
await _refreshTokenRepository.AddAsync(new RefreshToken()
{
Token = result.RefreshToken,
UserId = user.Id,
Role = user.Role,
Scope = scope,
Expires = refreshExpires,
IpAddress = ipAddress.ToString(),
CreatedAt = DateTime.UtcNow,
});
return result;
}
}