178 lines
6.7 KiB
C#
178 lines
6.7 KiB
C#
// <copyright file="AuthenticationService.cs" company="alveus.dev">
|
|
// Copyright (c) alveus.dev. All rights reserved. Licensed under the MIT License.
|
|
// </copyright>
|
|
|
|
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.Options;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
namespace Astral.Services.Services;
|
|
|
|
/// <inheritdoc />
|
|
[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<PasswordAuthenticateDto> _passwordAuthValidator;
|
|
private readonly JwtOptions _jwtOptions;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="AuthenticationService"/> class.
|
|
/// </summary>
|
|
/// <param name="cryptographyService">Instance of <see cref="ICryptographyService"/>.</param>
|
|
/// <param name="transactionProvider">Instance of <see cref="ITransactionProvider"/>.</param>
|
|
/// <param name="userRepository">Instance of <see cref="IUserRepository"/>.</param>
|
|
/// <param name="refreshTokenRepository">Instance of <see cref="IRefreshTokenRepository"/>.</param>
|
|
/// <param name="passwordAuthValidator">Instance of <see cref="IValidator{PasswordAuthenticateDto}"/>.</param>
|
|
/// <param name="jwtOptions">Instance of <see cref="IOptions{JwtOptions}"/>.</param>
|
|
public AuthenticationService(
|
|
ICryptographyService cryptographyService,
|
|
ITransactionProvider transactionProvider,
|
|
IUserRepository userRepository,
|
|
IRefreshTokenRepository refreshTokenRepository,
|
|
IValidator<PasswordAuthenticateDto> passwordAuthValidator,
|
|
IOptions<JwtOptions> jwtOptions)
|
|
{
|
|
_cryptographyService = cryptographyService;
|
|
_transactionProvider = transactionProvider;
|
|
_userRepository = userRepository;
|
|
_refreshTokenRepository = refreshTokenRepository;
|
|
_passwordAuthValidator = passwordAuthValidator;
|
|
_jwtOptions = jwtOptions.Value;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<SessionDto> 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);
|
|
|
|
transaction.Commit();
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate a new access token and refresh token for user.
|
|
/// </summary>
|
|
/// <param name="user">Instance of <see cref="User"/>.</param>
|
|
/// <param name="scope">Instance of <see cref="TokenScope"/>.</param>
|
|
/// <param name="ipAddress">The IP address of the agent requesting a user access token.</param>
|
|
/// <returns>Instance of <see cref="SessionDto"/>.</returns>
|
|
private async Task<SessionDto> GenerateAccessTokens(
|
|
User user,
|
|
TokenScope scope,
|
|
IPAddress ipAddress)
|
|
{
|
|
var handler = new JwtSecurityTokenHandler();
|
|
|
|
var claims = new List<Claim>
|
|
{
|
|
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;
|
|
}
|
|
}
|