authentication refresh fixed, movie creation page added

This commit is contained in:
2024-07-30 16:19:51 +02:00
Unverified
parent f9323b3d8c
commit 5b871714fa
63 changed files with 1568 additions and 200 deletions

View File

@@ -0,0 +1,85 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.Authorization;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Accounts;
namespace WatchIt.Website.Services.Utility.Authentication;
public class AuthenticationService : IAuthenticationService
{
#region SERVICES
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly HttpClient _httpClient;
private readonly ITokensService _tokensService;
private readonly IAccountsWebAPIService _accountsWebAPIService;
#endregion
#region CONSTRUCTORS
public AuthenticationService(AuthenticationStateProvider authenticationStateProvider, HttpClient httpClient, ITokensService tokensService, IAccountsWebAPIService accountsWebAPIService)
{
_authenticationStateProvider = authenticationStateProvider;
_httpClient = httpClient;
_tokensService = tokensService;
_accountsWebAPIService = accountsWebAPIService;
}
#endregion
#region PUBLIC METHODS
public async Task<User?> GetUserAsync()
{
AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (!GetAuthenticationStatusAsync(state))
{
return null;
}
return new User
{
Id = int.Parse(state.User.FindFirst(x => x.Type == JwtRegisteredClaimNames.Sub)!.Value),
Username = state.User.FindFirst(x => x.Type == JwtRegisteredClaimNames.UniqueName)!.Value,
Email = state.User.FindFirst(x => x.Type == JwtRegisteredClaimNames.Email)!.Value,
IsAdmin = bool.Parse(state.User.FindFirst(x => x.Type == "admin")!.Value),
};
}
public async Task<bool> GetAuthenticationStatusAsync()
{
AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync();
return GetAuthenticationStatusAsync(state);
}
public async Task LogoutAsync()
{
string? refreshToken = await _tokensService.GetRefreshToken();
if (refreshToken is not null)
{
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("refresh", refreshToken.Replace("\"", ""));
await _accountsWebAPIService.Logout();
_httpClient.DefaultRequestHeaders.Authorization = null;
}
}
#endregion
#region PRIVATE METHODS
private bool GetAuthenticationStatusAsync(AuthenticationState state)
{
return state.User.HasClaim(x => x.Type == JwtRegisteredClaimNames.Iss && x.Value == "WatchIt");
}
#endregion
}

View File

@@ -0,0 +1,8 @@
namespace WatchIt.Website.Services.Utility.Authentication;
public interface IAuthenticationService
{
Task<User?> GetUserAsync();
Task<bool> GetAuthenticationStatusAsync();
Task LogoutAsync();
}

View File

@@ -0,0 +1,151 @@
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using WatchIt.Common.Model.Accounts;
using WatchIt.Website.Services.WebAPI.Accounts;
namespace WatchIt.Website.Services.Utility.Tokens;
public class JWTAuthenticationStateProvider : AuthenticationStateProvider
{
#region SERVICES
private readonly HttpClient _httpClient;
private readonly ILogger<JWTAuthenticationStateProvider> _logger;
private readonly ITokensService _tokensService;
private readonly IAccountsWebAPIService _accountsService;
#endregion
#region CONSTRUCTORS
public JWTAuthenticationStateProvider(HttpClient httpClient, ILogger<JWTAuthenticationStateProvider> logger, ITokensService tokensService, IAccountsWebAPIService accountsService)
{
_httpClient = httpClient;
_logger = logger;
_tokensService = tokensService;
_accountsService = accountsService;
}
#endregion
#region PUBLIC METHODS
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
AuthenticationState state = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
Task<string?> accessTokenTask = _tokensService.GetAccessToken();
Task<string?> refreshTokenTask = _tokensService.GetRefreshToken();
await Task.WhenAll(accessTokenTask, refreshTokenTask);
string? accessToken = await accessTokenTask;
string? refreshToken = await refreshTokenTask;
bool refreshed = false;
if (string.IsNullOrWhiteSpace(accessToken))
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
return state;
}
string? accessTokenNew = await Refresh(refreshToken);
if (string.IsNullOrWhiteSpace(accessToken))
{
return state;
}
accessToken = accessTokenNew;
refreshed = true;
}
IEnumerable<Claim> claims = GetClaimsFromToken(accessToken);
Claim? expClaim = claims.FirstOrDefault(c => c.Type == "exp");
if (expClaim is not null && ConvertFromUnixTimestamp(int.Parse(expClaim.Value)) > DateTime.UtcNow)
{
if (refreshed)
{
return state;
}
}
else
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
return state;
}
string? accessTokenNew = await Refresh(refreshToken);
if (accessTokenNew is null)
{
return state;
}
accessToken = accessTokenNew;
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Replace("\"", ""));
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims)));
}
#endregion
#region PRIVATE METHODS
private async Task<string?> Refresh(string refreshToken)
{
AuthenticateResponse response = null;
await _accountsService.AuthenticateRefresh((data) => response = data);
await _tokensService.SaveAuthenticationData(response);
return response.AccessToken;
}
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)
{
throw new Exception("Incorrect token");
}
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
}
public static DateTime ConvertFromUnixTimestamp(int timestamp)
{
DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
return origin.AddSeconds(timestamp);
}
#endregion
}

View File

@@ -0,0 +1,13 @@
namespace WatchIt.Website.Services.Utility.Authentication;
public class User
{
#region PROPERTIES
public required long Id { get; init; }
public required string Username { get; init; }
public required string Email { get; init; }
public required bool IsAdmin { get; init; }
#endregion
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Accounts\WatchIt.Website.Services.WebAPI.Accounts.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.Utility.Tokens\WatchIt.Website.Services.Utility.Tokens.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,4 +6,6 @@ public class Accounts
public string Register { get; set; }
public string Authenticate { get; set; }
public string AuthenticateRefresh { get; set; }
public string Logout { get; set; }
public string GetProfilePicture { get; set; }
}

View File

@@ -4,5 +4,6 @@ public class ConfigurationData
{
public Logging Logging { get; set; }
public string AllowedHosts { get; set; }
public StorageKeys StorageKeys { get; set; }
public Endpoints Endpoints { get; set; }
}

View File

@@ -8,7 +8,4 @@ public class Movies
public string Post { get; set; }
public string Put { get; set; }
public string Delete { get; set; }
public string GetGenres { get; set; }
public string PostGenre { get; set; }
public string DeleteGenre { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace WatchIt.Website.Services.Utility.Configuration.Model;
public class StorageKeys
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}

View File

@@ -0,0 +1,15 @@
using WatchIt.Common.Model.Accounts;
namespace WatchIt.Website.Services.Utility.Tokens;
public interface ITokensService
{
Task<string?> GetAccessToken();
Task<string?> GetRefreshToken();
Task SaveAuthenticationData(AuthenticateResponse authenticateResponse);
Task SaveAccessToken(string accessToken);
Task SaveRefreshToken(string refreshToken);
Task RemoveAuthenticationData();
Task RemoveAccessToken();
Task RemoveRefreshToken();
}

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using WatchIt.Common.Model.Accounts;
using WatchIt.Website.Services.Utility.Configuration;
namespace WatchIt.Website.Services.Utility.Tokens;
public class TokensService : ITokensService
{
#region SERVICES
private readonly ProtectedLocalStorage _localStorageService;
private readonly IConfigurationService _configurationService;
#endregion
#region CONSTRUCTORS
public TokensService(ProtectedLocalStorage localStorageService, IConfigurationService configurationService)
{
_localStorageService = localStorageService;
_configurationService = configurationService;
}
#endregion
#region PUBLIC METHODS
public async Task<string?> GetAccessToken() => await GetValueAsync<string>(GetAccessTokenStorageKey());
public async Task<string?> GetRefreshToken() => await GetValueAsync<string>(GetRefreshTokenStorageKey());
public async Task SaveAuthenticationData(AuthenticateResponse authenticateResponse) => await Task.WhenAll(SaveAccessToken(authenticateResponse.AccessToken), SaveRefreshToken(authenticateResponse.RefreshToken));
public async Task SaveAccessToken(string accessToken) => await _localStorageService.SetAsync(GetAccessTokenStorageKey(), accessToken);
public async Task SaveRefreshToken(string refreshToken) => await _localStorageService.SetAsync(GetRefreshTokenStorageKey(), refreshToken);
public async Task RemoveAuthenticationData() => await Task.WhenAll(RemoveAccessToken(), RemoveRefreshToken());
public async Task RemoveAccessToken() => await _localStorageService.DeleteAsync(GetAccessTokenStorageKey());
public async Task RemoveRefreshToken() => await _localStorageService.DeleteAsync(GetRefreshTokenStorageKey());
#endregion
#region PRIVATE METHODS
private string GetAccessTokenStorageKey() => _configurationService.Data.StorageKeys.AccessToken;
private string GetRefreshTokenStorageKey() => _configurationService.Data.StorageKeys.RefreshToken;
private async Task<T?> GetValueAsync<T>(string key)
{
ProtectedBrowserStorageResult<T> result = await _localStorageService.GetAsync<T>(key);
return result.Success ? result.Value : default;
}
#endregion
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WatchIt.Website.Services.Utility.Token</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Model\WatchIt.Common.Model.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.Utility.Configuration\WatchIt.Website.Services.Utility.Configuration.csproj" />
</ItemGroup>
</Project>