mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 03:48:56 +00:00
Workshop - Added login UI
This commit is contained in:
1 parent
7eccdf079a
commit
990f55b7c5
5 files changed
+274
-113
No files matched your search
@@ -1,6 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Security.Claims;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
@@ -12,29 +13,31 @@ namespace Artemis.WebClient.Workshop.Services;
|
||||
|
||||
public interface IAuthenticationService : IProtectedArtemisService
|
||||
{
|
||||
bool IsLoggedIn { get; }
|
||||
string? UserCode { get; }
|
||||
IObservable<bool> IsLoggedIn { get; }
|
||||
IObservable<string?> UserCode { get; }
|
||||
IObservable<string?> VerificationUri { get; }
|
||||
ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||
|
||||
Task<string?> GetBearer();
|
||||
Task<bool> AutoLogin();
|
||||
Task<bool> Login();
|
||||
Task<bool> Login(CancellationToken cancellationToken);
|
||||
void Logout();
|
||||
}
|
||||
|
||||
internal class AuthenticationService : CorePropertyChanged, IAuthenticationService
|
||||
{
|
||||
private const string CLIENT_ID = "artemis.desktop";
|
||||
private readonly IAuthenticationRepository _authenticationRepository;
|
||||
private readonly SemaphoreSlim _bearerLock = new(1);
|
||||
private readonly ObservableCollection<Claim> _claims = new();
|
||||
|
||||
private readonly IDiscoveryCache _discoveryCache;
|
||||
private readonly IAuthenticationRepository _authenticationRepository;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ObservableCollection<Claim> _claims = new();
|
||||
private readonly SemaphoreSlim _bearerLock = new(1);
|
||||
private readonly BehaviorSubject<bool> _isLoggedInSubject = new(false);
|
||||
private readonly BehaviorSubject<string?> _userCodeSubject = new(null);
|
||||
private readonly BehaviorSubject<string?> _verificationUriSubject = new(null);
|
||||
|
||||
private AuthenticationToken? _token;
|
||||
private string? _userCode;
|
||||
private bool _isLoggedIn;
|
||||
|
||||
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
||||
{
|
||||
@@ -45,19 +48,85 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
||||
Claims = new ReadOnlyObservableCollection<Claim>(_claims);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLoggedIn
|
||||
private async Task<DiscoveryDocumentResponse> GetDiscovery()
|
||||
{
|
||||
get => _isLoggedIn;
|
||||
private set => SetAndNotify(ref _isLoggedIn, value);
|
||||
DiscoveryDocumentResponse disco = await _discoveryCache.GetAsync();
|
||||
if (disco.IsError)
|
||||
throw new ArtemisWebClientException("Failed to retrieve discovery document: " + disco.Error);
|
||||
|
||||
return disco;
|
||||
}
|
||||
|
||||
private async Task<bool> AttemptRequestDeviceToken(HttpClient client, string deviceCode, CancellationToken cancellationToken)
|
||||
{
|
||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||
TokenResponse response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest
|
||||
{
|
||||
Address = disco.TokenEndpoint,
|
||||
ClientId = CLIENT_ID,
|
||||
DeviceCode = deviceCode
|
||||
}, cancellationToken);
|
||||
|
||||
if (response.IsError)
|
||||
{
|
||||
if (response.Error == OidcConstants.TokenErrors.AuthorizationPending || response.Error == OidcConstants.TokenErrors.SlowDown)
|
||||
return false;
|
||||
|
||||
throw new ArtemisWebClientException("Failed to request device token: " + response.Error);
|
||||
}
|
||||
|
||||
SetCurrentUser(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetCurrentUser(TokenResponse response)
|
||||
{
|
||||
_token = new AuthenticationToken(response);
|
||||
_authenticationRepository.SetRefreshToken(_token.RefreshToken);
|
||||
|
||||
JwtSecurityTokenHandler handler = new();
|
||||
JwtSecurityToken? token = handler.ReadJwtToken(response.IdentityToken);
|
||||
if (token == null)
|
||||
throw new ArtemisWebClientException("Failed to read JWT token");
|
||||
|
||||
_claims.Clear();
|
||||
foreach (Claim responseClaim in token.Claims)
|
||||
_claims.Add(responseClaim);
|
||||
|
||||
_isLoggedInSubject.OnNext(true);
|
||||
}
|
||||
|
||||
private async Task<bool> UseRefreshToken(string refreshToken)
|
||||
{
|
||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||
HttpClient client = _httpClientFactory.CreateClient();
|
||||
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
|
||||
{
|
||||
Address = disco.TokenEndpoint,
|
||||
ClientId = CLIENT_ID,
|
||||
RefreshToken = refreshToken
|
||||
});
|
||||
|
||||
if (response.IsError)
|
||||
{
|
||||
if (response.Error == OidcConstants.TokenErrors.ExpiredToken)
|
||||
return false;
|
||||
|
||||
throw new ArtemisWebClientException("Failed to request refresh token: " + response.Error);
|
||||
}
|
||||
|
||||
SetCurrentUser(response);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? UserCode
|
||||
{
|
||||
get => _userCode;
|
||||
private set => SetAndNotify(ref _userCode, value);
|
||||
}
|
||||
public IObservable<bool> IsLoggedIn => _isLoggedInSubject.AsObservable();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IObservable<string?> UserCode => _userCodeSubject.AsObservable();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IObservable<string?> VerificationUri => _verificationUriSubject.AsObservable();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||
@@ -69,7 +138,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
||||
try
|
||||
{
|
||||
// If not logged in, attempt to auto login first
|
||||
if (!IsLoggedIn)
|
||||
if (!_isLoggedInSubject.Value)
|
||||
await AutoLogin();
|
||||
|
||||
if (_token == null)
|
||||
@@ -90,9 +159,9 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AutoLogin()
|
||||
{
|
||||
if (IsLoggedIn)
|
||||
if (_isLoggedInSubject.Value)
|
||||
return true;
|
||||
|
||||
|
||||
string? refreshToken = _authenticationRepository.GetRefreshToken();
|
||||
if (refreshToken == null)
|
||||
return false;
|
||||
@@ -101,34 +170,44 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> Login()
|
||||
public async Task<bool> Login(CancellationToken cancellationToken)
|
||||
{
|
||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||
HttpClient client = _httpClientFactory.CreateClient();
|
||||
DeviceAuthorizationResponse response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest
|
||||
{
|
||||
Address = disco.DeviceAuthorizationEndpoint,
|
||||
ClientId = CLIENT_ID,
|
||||
ClientId = CLIENT_ID,
|
||||
Scope = "openid profile email offline_access api"
|
||||
});
|
||||
}, cancellationToken);
|
||||
if (response.IsError)
|
||||
throw new ArtemisWebClientException("Failed to request device authorization: " + response.Error);
|
||||
if (response.DeviceCode == null)
|
||||
throw new ArtemisWebClientException("Failed to request device authorization: Got no device code");
|
||||
|
||||
DateTimeOffset timeout = DateTimeOffset.UtcNow.AddSeconds(response.ExpiresIn ?? 1800);
|
||||
_userCodeSubject.OnNext(response.UserCode);
|
||||
_verificationUriSubject.OnNext(response.VerificationUri);
|
||||
|
||||
Process.Start(new ProcessStartInfo {FileName = response.VerificationUriComplete, UseShellExecute = true});
|
||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval));
|
||||
while (DateTimeOffset.UtcNow < timeout)
|
||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval), cancellationToken);
|
||||
try
|
||||
{
|
||||
bool success = await AttemptRequestDeviceToken(client, response.DeviceCode);
|
||||
if (success)
|
||||
return true;
|
||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval));
|
||||
}
|
||||
while (DateTimeOffset.UtcNow < timeout)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
bool success = await AttemptRequestDeviceToken(client, response.DeviceCode, cancellationToken);
|
||||
if (success)
|
||||
return true;
|
||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval), cancellationToken);
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_userCodeSubject.OnNext(null);
|
||||
_verificationUriSubject.OnNext(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -137,78 +216,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
||||
_token = null;
|
||||
_claims.Clear();
|
||||
_authenticationRepository.SetRefreshToken(null);
|
||||
|
||||
IsLoggedIn = false;
|
||||
}
|
||||
|
||||
private async Task<DiscoveryDocumentResponse> GetDiscovery()
|
||||
{
|
||||
DiscoveryDocumentResponse disco = await _discoveryCache.GetAsync();
|
||||
if (disco.IsError)
|
||||
throw new ArtemisWebClientException("Failed to retrieve discovery document: " + disco.Error);
|
||||
|
||||
return disco;
|
||||
}
|
||||
|
||||
private async Task<bool> AttemptRequestDeviceToken(HttpClient client, string deviceCode)
|
||||
{
|
||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||
TokenResponse response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest
|
||||
{
|
||||
Address = disco.TokenEndpoint,
|
||||
ClientId = CLIENT_ID,
|
||||
DeviceCode = deviceCode
|
||||
});
|
||||
|
||||
if (response.IsError)
|
||||
{
|
||||
if (response.Error == OidcConstants.TokenErrors.AuthorizationPending || response.Error == OidcConstants.TokenErrors.SlowDown)
|
||||
return false;
|
||||
|
||||
throw new ArtemisWebClientException("Failed to request device token: " + response.Error);
|
||||
}
|
||||
|
||||
SetCurrentUser(response);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetCurrentUser(TokenResponse response)
|
||||
{
|
||||
_token = new AuthenticationToken(response);
|
||||
_authenticationRepository.SetRefreshToken(_token.RefreshToken);
|
||||
|
||||
JwtSecurityTokenHandler handler = new();
|
||||
JwtSecurityToken? token = handler.ReadJwtToken(response.IdentityToken);
|
||||
if (token == null)
|
||||
throw new ArtemisWebClientException("Failed to read JWT token");
|
||||
|
||||
_claims.Clear();
|
||||
foreach (Claim responseClaim in token.Claims)
|
||||
_claims.Add(responseClaim);
|
||||
|
||||
IsLoggedIn = true;
|
||||
}
|
||||
|
||||
private async Task<bool> UseRefreshToken(string refreshToken)
|
||||
{
|
||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||
HttpClient client = _httpClientFactory.CreateClient();
|
||||
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest()
|
||||
{
|
||||
Address = disco.TokenEndpoint,
|
||||
ClientId = CLIENT_ID,
|
||||
RefreshToken = refreshToken
|
||||
});
|
||||
|
||||
if (response.IsError)
|
||||
{
|
||||
if (response.Error == OidcConstants.TokenErrors.ExpiredToken)
|
||||
return false;
|
||||
|
||||
throw new ArtemisWebClientException("Failed to request refresh token: " + response.Error);
|
||||
}
|
||||
|
||||
SetCurrentUser(response);
|
||||
return false;
|
||||
_isLoggedInSubject.OnNext(false);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user