Refactoring, database structure changed
This commit is contained in:
164
WatchIt.Website/Services/Authentication/AuthenticationService.cs
Normal file
164
WatchIt.Website/Services/Authentication/AuthenticationService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Refit;
|
||||
using WatchIt.DTO.Models.Controllers.Accounts.Account;
|
||||
using WatchIt.DTO.Models.Controllers.Accounts.AccountLogout;
|
||||
using WatchIt.DTO.Models.Controllers.Authentication;
|
||||
using WatchIt.Website.Clients;
|
||||
using WatchIt.Website.Services.Tokens;
|
||||
|
||||
namespace WatchIt.Website.Services.Authentication;
|
||||
|
||||
public class AuthenticationService : IAuthenticationService
|
||||
{
|
||||
#region FIELDS
|
||||
|
||||
private readonly ITokensService _tokensService;
|
||||
private readonly IAuthenticationClient _authenticationClient;
|
||||
private readonly IAccountsClient _accountsClient;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public AuthenticationService(ITokensService tokensService, IAuthenticationClient authenticationClient, IAccountsClient accountsClient)
|
||||
{
|
||||
_tokensService = tokensService;
|
||||
_authenticationClient = authenticationClient;
|
||||
_accountsClient = accountsClient;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
public async Task<string?> GetRawAccessTokenAsync()
|
||||
{
|
||||
string? accessToken = await _tokensService.GetAccessToken();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ValidateToken(accessToken))
|
||||
{
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
string? refreshToken = await _tokensService.GetRefreshToken();
|
||||
if (string.IsNullOrWhiteSpace(refreshToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IApiResponse<AuthenticationResponse> refreshResponse = await _authenticationClient.AuthenticateRefresh(new AuthenticationRefreshRequest
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken
|
||||
});
|
||||
if (refreshResponse.IsSuccessful)
|
||||
{
|
||||
await UpdateTokens(refreshResponse.Content);
|
||||
return refreshResponse.Content.AccessToken;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<long?> GetAccountIdAsync()
|
||||
{
|
||||
string? accessToken = await GetRawAccessTokenAsync();
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IEnumerable<Claim> claims = GetClaimsFromToken(accessToken);
|
||||
Claim? subClaim = claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Sub);
|
||||
if (subClaim is null || !long.TryParse(subClaim.Value, out long accountId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public async Task<IApiResponse> Login(AuthenticationRequest data)
|
||||
{
|
||||
IApiResponse<AuthenticationResponse> response = await _authenticationClient.Authenticate(data);
|
||||
if (response.IsSuccessful)
|
||||
{
|
||||
await UpdateTokens(response.Content);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<IApiResponse> Logout()
|
||||
{
|
||||
IApiResponse response = await _accountsClient.Logout(new AccountLogoutRequest
|
||||
{
|
||||
RefreshToken = await _tokensService.GetRefreshToken(),
|
||||
});
|
||||
if (response.IsSuccessful)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
[
|
||||
_tokensService.DeleteAccessToken(),
|
||||
_tokensService.DeleteRefreshToken()
|
||||
]);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private async Task UpdateTokens(AuthenticationResponse tokens) => await Task.WhenAll(
|
||||
[
|
||||
_tokensService.SetAccessToken(tokens.AccessToken),
|
||||
_tokensService.SetRefreshToken(tokens.RefreshToken)
|
||||
]);
|
||||
|
||||
private static bool ValidateToken(string token)
|
||||
{
|
||||
IEnumerable<Claim> claims = GetClaimsFromToken(token);
|
||||
Claim? claim = claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp);
|
||||
if (claim is null || !long.TryParse(claim.Value, out long expiration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
DateTime expirationDate = DateTime.UnixEpoch.AddSeconds(expiration);
|
||||
return expirationDate > DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private static IEnumerable<Claim> GetClaimsFromToken(string token)
|
||||
{
|
||||
string payload = token.Split('.')[1];
|
||||
|
||||
switch (payload.Length % 4)
|
||||
{
|
||||
case 2: payload += "=="; break;
|
||||
case 3: payload += "="; break;
|
||||
}
|
||||
|
||||
byte[] jsonBytes = Convert.FromBase64String(payload);
|
||||
Dictionary<string, object>? keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
|
||||
|
||||
if (keyValuePairs is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace WatchIt.Website.Services.Authentication;
|
||||
|
||||
public static class AuthenticationTokenProvider
|
||||
{
|
||||
private static Func<CancellationToken, Task<string>>? _getTokenAsyncFunc;
|
||||
|
||||
public static void SetTokenGetterFunc(Func<CancellationToken, Task<string>> getTokenAsyncFunc)
|
||||
{
|
||||
_getTokenAsyncFunc = getTokenAsyncFunc;
|
||||
}
|
||||
|
||||
public static Task<string> GetTokenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_getTokenAsyncFunc is null)
|
||||
{
|
||||
throw new InvalidOperationException("Token getter func must be set before using it");
|
||||
}
|
||||
return _getTokenAsyncFunc!(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Refit;
|
||||
using WatchIt.DTO.Models.Controllers.Authentication;
|
||||
|
||||
namespace WatchIt.Website.Services.Authentication;
|
||||
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
Task<string?> GetRawAccessTokenAsync();
|
||||
Task<long?> GetAccountIdAsync();
|
||||
Task<IApiResponse> Login(AuthenticationRequest data);
|
||||
Task<IApiResponse> Logout();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace WatchIt.Website.Services.Tokens.Configuration;
|
||||
|
||||
public class StorageKeys
|
||||
{
|
||||
public string AccessToken { get; set; } = null!;
|
||||
public string RefreshToken { get; set; } = null!;
|
||||
}
|
||||
6
WatchIt.Website/Services/Tokens/Configuration/Tokens.cs
Normal file
6
WatchIt.Website/Services/Tokens/Configuration/Tokens.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace WatchIt.Website.Services.Tokens.Configuration;
|
||||
|
||||
public class Tokens
|
||||
{
|
||||
public StorageKeys StorageKeys { get; set; } = null!;
|
||||
}
|
||||
20
WatchIt.Website/Services/Tokens/ITokensService.cs
Normal file
20
WatchIt.Website/Services/Tokens/ITokensService.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace WatchIt.Website.Services.Tokens;
|
||||
|
||||
public interface ITokensService
|
||||
{
|
||||
#region Access token
|
||||
|
||||
Task<string?> GetAccessToken();
|
||||
Task SetAccessToken(string accessToken);
|
||||
Task DeleteAccessToken();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresh token
|
||||
|
||||
Task<string?> GetRefreshToken();
|
||||
Task SetRefreshToken(string accessToken);
|
||||
Task DeleteRefreshToken();
|
||||
|
||||
#endregion
|
||||
}
|
||||
76
WatchIt.Website/Services/Tokens/TokensService.cs
Normal file
76
WatchIt.Website/Services/Tokens/TokensService.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace WatchIt.Website.Services.Tokens;
|
||||
|
||||
public class TokensService : ITokensService
|
||||
{
|
||||
#region SERVICES
|
||||
|
||||
private readonly ILogger<TokensService> _logger;
|
||||
private readonly ProtectedLocalStorage _localStorageService;
|
||||
private readonly Configuration.Tokens _configuration;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region CONSTRUCTORS
|
||||
|
||||
public TokensService(ILogger<TokensService> logger, IConfiguration configuration, ProtectedLocalStorage localStorageService)
|
||||
{
|
||||
_logger = logger;
|
||||
_localStorageService = localStorageService;
|
||||
_configuration = configuration.GetSection("Tokens").Get<Configuration.Tokens>()!;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PUBLIC METHODS
|
||||
|
||||
#region Access token
|
||||
|
||||
public async Task<string?> GetAccessToken() => await GetValueAsync<string>(_configuration.StorageKeys.AccessToken);
|
||||
|
||||
public async Task SetAccessToken(string accessToken) => await _localStorageService.SetAsync(_configuration.StorageKeys.AccessToken, accessToken);
|
||||
|
||||
public async Task DeleteAccessToken() => await _localStorageService.DeleteAsync(_configuration.StorageKeys.AccessToken);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresh token
|
||||
|
||||
public async Task<string?> GetRefreshToken() => await GetValueAsync<string>(_configuration.StorageKeys.RefreshToken);
|
||||
|
||||
public async Task SetRefreshToken(string accessToken) => await _localStorageService.SetAsync(_configuration.StorageKeys.RefreshToken, accessToken);
|
||||
|
||||
public async Task DeleteRefreshToken() => await _localStorageService.DeleteAsync(_configuration.StorageKeys.RefreshToken);
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region PRIVATE METHODS
|
||||
|
||||
private async Task<T?> GetValueAsync<T>(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProtectedBrowserStorageResult<T> result = await _localStorageService.GetAsync<T>(key);
|
||||
return result.Success ? result.Value : default;
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Browser storage error has occurred. Deleting value.");
|
||||
await _localStorageService.DeleteAsync(key);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user