2024-10-31 04:12:02 +03:00
using Microsoft.AspNetCore.Http ;
using Microsoft.Extensions.Logging ;
using Mirea.Api.Security.Common ;
using Mirea.Api.Security.Common.Domain ;
2024-11-02 23:34:23 +03:00
using Mirea.Api.Security.Common.Domain.Caching ;
2024-05-29 05:50:47 +03:00
using Mirea.Api.Security.Common.Interfaces ;
using System ;
2024-05-29 05:51:32 +03:00
using System.Security ;
using System.Text.Json ;
using System.Threading ;
using System.Threading.Tasks ;
2024-05-29 05:32:22 +03:00
namespace Mirea.Api.Security.Services ;
2024-10-31 04:12:02 +03:00
public class AuthService ( ICacheService cache , IAccessToken accessTokenService , IRevokedToken revokedToken , ILogger < AuthService > logger , PasswordHashService passwordService )
2024-05-29 05:32:22 +03:00
{
public TimeSpan Lifetime { private get ; init ; }
2024-10-31 04:12:02 +03:00
public TimeSpan LifetimeFirstAuth { private get ; init ; }
2024-05-29 05:35:44 +03:00
private static string GenerateRefreshToken ( ) = > Guid . NewGuid ( ) . ToString ( ) . Replace ( "-" , "" ) +
GeneratorKey . GenerateString ( 32 ) ;
2024-05-29 05:51:03 +03:00
private ( string Token , DateTime ExpireIn ) GenerateAccessToken ( string userId ) = >
accessTokenService . GenerateToken ( userId ) ;
2024-05-29 05:36:26 +03:00
private static string GetAuthCacheKey ( string fingerprint ) = > $"{fingerprint}_auth_token" ;
2024-11-02 22:10:46 +03:00
internal static string GetFirstAuthCacheKey ( string fingerprint ) = > $"{fingerprint}_auth_token_first" ;
2024-05-29 05:50:47 +03:00
2024-10-31 04:12:02 +03:00
private Task SetAuthTokenDataToCache ( AuthToken data , CancellationToken cancellation ) = >
2024-05-29 05:50:47 +03:00
cache . SetAsync (
2024-10-31 04:12:02 +03:00
GetAuthCacheKey ( data . Fingerprint ) ,
2024-05-29 05:50:47 +03:00
JsonSerializer . SerializeToUtf8Bytes ( data ) ,
slidingExpiration : Lifetime ,
cancellationToken : cancellation ) ;
2024-05-29 05:51:32 +03:00
2024-05-29 05:55:31 +03:00
private Task RevokeAccessToken ( string token ) = >
revokedToken . AddTokenToRevokedAsync ( token , accessTokenService . GetExpireDateTime ( token ) ) ;
2024-10-31 04:12:02 +03:00
private async Task VerifyUserOrThrowError ( RequestContextInfo requestContext , User user , string password ,
CancellationToken cancellation = default )
{
if ( passwordService . VerifyPassword ( password , user . Salt , user . PasswordHash ) )
return ;
var failedLoginCacheName = $"{requestContext.Fingerprint}_login_failed" ;
var countFailedLogin = await cache . GetAsync < int? > ( failedLoginCacheName , cancellation ) ? ? 1 ;
var cacheSaveTime = TimeSpan . FromHours ( 1 ) ;
await cache . SetAsync ( failedLoginCacheName , countFailedLogin + 1 , slidingExpiration : cacheSaveTime , cancellationToken : cancellation ) ;
if ( countFailedLogin > 5 )
{
logger . LogWarning (
2024-11-02 22:10:46 +03:00
"Multiple unsuccessful login attempts for user ID {UserId} from IP {UserIp}. Attempt count: {AttemptNumber}." ,
2024-10-31 04:12:02 +03:00
user . Id ,
requestContext . Ip ,
countFailedLogin ) ;
2024-11-02 22:10:46 +03:00
throw new SecurityException ( "Too many unsuccessful login attempts. Please try again later." ) ;
2024-10-31 04:12:02 +03:00
}
logger . LogInformation (
2024-11-02 22:10:46 +03:00
"Login attempt failed for user ID {UserId}. IP: {UserIp}, User-Agent: {UserAgent}, Fingerprint: {Fingerprint}. Attempt count: {AttemptNumber}." ,
2024-10-31 04:12:02 +03:00
user . Id ,
requestContext . Ip ,
requestContext . UserAgent ,
requestContext . Fingerprint ,
countFailedLogin ) ;
2024-11-02 22:10:46 +03:00
throw new SecurityException ( "Authentication failed. Please check your credentials." ) ;
2024-10-31 04:12:02 +03:00
}
private async Task GenerateAuthTokensAsync ( CookieOptionsParameters cookieOptions , HttpContext context , RequestContextInfo requestContext , string userId , CancellationToken cancellation = default )
2024-05-29 05:51:32 +03:00
{
var refreshToken = GenerateRefreshToken ( ) ;
2024-06-21 21:36:11 +03:00
var ( token , expireIn ) = GenerateAccessToken ( userId ) ;
2024-05-29 05:51:32 +03:00
2024-10-31 04:12:02 +03:00
var authToken = new AuthToken ( requestContext )
2024-05-29 05:51:32 +03:00
{
CreatedAt = DateTime . UtcNow ,
2024-10-31 04:12:22 +03:00
RefreshToken = refreshToken ,
2024-05-29 05:51:32 +03:00
UserId = userId ,
2024-10-31 04:12:22 +03:00
AccessToken = token
2024-05-29 05:51:32 +03:00
} ;
2024-10-31 04:12:02 +03:00
await SetAuthTokenDataToCache ( authToken , cancellation ) ;
2024-11-02 00:51:27 +03:00
cookieOptions . SetCookie ( context , CookieNames . AccessToken , authToken . AccessToken , expireIn ) ;
2024-10-31 04:12:02 +03:00
cookieOptions . SetCookie ( context , CookieNames . RefreshToken , authToken . RefreshToken , DateTime . UtcNow . Add ( Lifetime ) ) ;
logger . LogInformation (
2024-11-02 22:10:46 +03:00
"Login successful for user ID {UserId}. IP: {UserIp}, User-Agent: {UserAgent}, Fingerprint: {Fingerprint}." ,
2024-10-31 04:12:02 +03:00
authToken . UserId ,
authToken . Ip ,
authToken . UserAgent ,
authToken . Fingerprint ) ;
}
2024-11-02 01:09:15 +03:00
public async Task < bool > LoginAsync ( CookieOptionsParameters cookieOptions , HttpContext context , TwoFactorAuthenticator authenticator , string code , CancellationToken cancellation = default )
2024-10-31 04:12:02 +03:00
{
var requestContext = new RequestContextInfo ( context , cookieOptions ) ;
2024-11-02 01:09:15 +03:00
var firstTokenAuth = await cache . GetAsync < FirstAuthToken ? > ( GetFirstAuthCacheKey ( requestContext . Fingerprint ) , cancellationToken : cancellation ) ;
if ( firstTokenAuth = = null | | authenticator ! = firstTokenAuth . TwoFactorAuthenticator )
2024-11-02 22:10:46 +03:00
throw new SecurityException ( "Session expired. Please log in again." ) ;
2024-05-29 05:51:32 +03:00
2024-11-02 00:51:27 +03:00
switch ( firstTokenAuth . TwoFactorAuthenticator )
2024-05-29 05:51:32 +03:00
{
2024-11-02 00:51:27 +03:00
case TwoFactorAuthenticator . Totp :
2024-10-31 04:12:02 +03:00
{
if ( string . IsNullOrEmpty ( firstTokenAuth . Secret ) )
2024-11-02 22:10:46 +03:00
throw new InvalidOperationException ( "Required authentication data is missing." ) ;
2024-10-31 04:12:02 +03:00
var totp = new TotpService ( firstTokenAuth . Secret ) ;
if ( ! totp . VerifyToken ( code ) )
2024-11-02 22:10:46 +03:00
throw new SecurityException ( "Invalid verification code. Please try again." ) ;
2024-10-31 04:12:02 +03:00
}
break ;
2024-11-02 22:10:46 +03:00
case TwoFactorAuthenticator . None :
break ;
2024-10-31 04:12:02 +03:00
default :
2024-11-02 22:10:46 +03:00
throw new InvalidOperationException ( "Unsupported authorization method." ) ;
2024-10-31 04:12:02 +03:00
}
await GenerateAuthTokensAsync ( cookieOptions , context , requestContext , firstTokenAuth . UserId , cancellation ) ;
return true ;
2024-05-29 05:51:32 +03:00
}
2024-05-29 05:55:57 +03:00
2024-11-02 01:06:58 +03:00
public async Task < TwoFactorAuthenticator > LoginAsync ( CookieOptionsParameters cookieOptions , User user , HttpContext context , string password , CancellationToken cancellation = default )
2024-10-31 04:12:02 +03:00
{
var requestContext = new RequestContextInfo ( context , cookieOptions ) ;
await VerifyUserOrThrowError ( requestContext , user , password , cancellation ) ;
2024-11-02 00:51:27 +03:00
if ( user . TwoFactorAuthenticator = = TwoFactorAuthenticator . None )
2024-10-31 04:12:02 +03:00
{
2024-11-02 20:21:46 +03:00
await GenerateAuthTokensAsync ( cookieOptions , context , requestContext , user . Id , cancellation ) ;
2024-11-02 01:06:58 +03:00
return TwoFactorAuthenticator . None ;
2024-10-31 04:12:02 +03:00
}
var firstAuthToken = new FirstAuthToken ( requestContext )
{
2024-11-02 20:21:46 +03:00
UserId = user . Id ,
2024-10-31 04:12:02 +03:00
Secret = user . SecondFactorToken ,
2024-11-02 00:51:27 +03:00
TwoFactorAuthenticator = user . TwoFactorAuthenticator
2024-10-31 04:12:02 +03:00
} ;
await cache . SetAsync ( GetFirstAuthCacheKey ( requestContext . Fingerprint ) , firstAuthToken , absoluteExpirationRelativeToNow : LifetimeFirstAuth , cancellationToken : cancellation ) ;
2024-05-29 06:00:15 +03:00
2024-11-02 01:06:58 +03:00
return user . TwoFactorAuthenticator ;
2024-10-31 04:12:02 +03:00
}
public async Task RefreshTokenAsync ( CookieOptionsParameters cookieOptions , HttpContext context , CancellationToken cancellation = default )
2024-05-29 05:55:57 +03:00
{
2024-10-31 04:12:02 +03:00
var requestContext = new RequestContextInfo ( context , cookieOptions ) ;
var authToken = await cache . GetAsync < AuthToken > ( GetAuthCacheKey ( requestContext . Fingerprint ) , cancellation )
? ? throw new SecurityException ( "The session time has expired" ) ;
2024-05-29 05:55:57 +03:00
2024-10-31 04:12:02 +03:00
if ( authToken . RefreshToken ! = requestContext . RefreshToken | |
authToken . UserAgent ! = requestContext . UserAgent & &
authToken . Ip ! = requestContext . Ip )
2024-05-29 05:55:57 +03:00
{
await RevokeAccessToken ( authToken . AccessToken ) ;
2024-10-31 04:12:02 +03:00
await cache . RemoveAsync ( GetAuthCacheKey ( requestContext . Fingerprint ) , cancellation ) ;
cookieOptions . DropCookie ( context , CookieNames . AccessToken ) ;
cookieOptions . DropCookie ( context , CookieNames . RefreshToken ) ;
2024-11-02 22:10:46 +03:00
logger . LogWarning ( "Token validation failed for user ID {UserId}. IP: {UserIp}, User-Agent: {UserAgent}, Fingerprint: {Fingerprint}. Reason: {Reason}." ,
2024-10-31 04:12:02 +03:00
authToken . UserId ,
authToken . Ip ,
authToken . UserAgent ,
authToken . Fingerprint ,
authToken . RefreshToken ! = requestContext . RefreshToken ?
$"Cached refresh token '{authToken.RefreshToken}' does not match the provided refresh token '{requestContext.RefreshToken}'" :
$"User-Agent '{authToken.UserAgent}' and IP '{authToken.Ip}' in cache do not match the provided User-Agent '{requestContext.UserAgent}' and IP '{requestContext.Ip}'" ) ;
throw new SecurityException ( "The session time has expired" ) ;
2024-05-29 05:55:57 +03:00
}
2024-06-21 21:36:11 +03:00
var ( token , expireIn ) = GenerateAccessToken ( authToken . UserId ) ;
2024-05-29 05:55:57 +03:00
await RevokeAccessToken ( authToken . AccessToken ) ;
2024-06-28 22:52:05 +03:00
var newRefreshToken = GenerateRefreshToken ( ) ;
2024-06-21 21:36:11 +03:00
authToken . AccessToken = token ;
2024-06-28 22:52:05 +03:00
authToken . RefreshToken = newRefreshToken ;
2024-10-31 04:12:02 +03:00
await SetAuthTokenDataToCache ( authToken , cancellation ) ;
2024-11-02 00:51:27 +03:00
cookieOptions . SetCookie ( context , CookieNames . AccessToken , authToken . AccessToken , expireIn ) ;
2024-10-31 04:12:02 +03:00
cookieOptions . SetCookie ( context , CookieNames . RefreshToken , authToken . RefreshToken , DateTime . UtcNow . Add ( Lifetime ) ) ;
2024-05-29 05:55:57 +03:00
}
2024-05-29 05:56:27 +03:00
2024-10-31 04:12:02 +03:00
public async Task LogoutAsync ( CookieOptionsParameters cookieOptions , HttpContext context , CancellationToken cancellation = default )
2024-05-29 05:56:27 +03:00
{
2024-10-31 04:12:02 +03:00
var requestContext = new RequestContextInfo ( context , cookieOptions ) ;
var authTokenStruct = await cache . GetAsync < AuthToken > ( GetAuthCacheKey ( requestContext . Fingerprint ) , cancellation ) ;
2024-05-29 05:56:27 +03:00
2024-10-31 04:12:02 +03:00
if ( authTokenStruct = = null )
return ;
2024-05-29 05:56:27 +03:00
2024-10-31 04:12:02 +03:00
await RevokeAccessToken ( authTokenStruct . AccessToken ) ;
await cache . RemoveAsync ( requestContext . Fingerprint , cancellation ) ;
cookieOptions . DropCookie ( context , CookieNames . AccessToken ) ;
cookieOptions . DropCookie ( context , CookieNames . RefreshToken ) ;
2024-05-29 05:56:27 +03:00
}
2024-05-29 05:32:22 +03:00
}