authentication refresh fixed, movie creation page added
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace WatchIt.Website.Services.Utility.Authentication;
|
||||
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
Task<User?> GetUserAsync();
|
||||
Task<bool> GetAuthenticationStatusAsync();
|
||||
Task LogoutAsync();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace WatchIt.Website.Services.Utility.Configuration.Model;
|
||||
|
||||
public class StorageKeys
|
||||
{
|
||||
public string AccessToken { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user