mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 13:28:33 +00:00
Workshop login - Use code auth flow instead of device code
This commit is contained in:
parent
990f55b7c5
commit
eeaf3999cf
@ -2,7 +2,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:currentUser="clr-namespace:Artemis.UI.Screens.Workshop.CurrentUser"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Workshop.CurrentUser.WorkshopLoginView"
|
||||
@ -15,22 +14,7 @@
|
||||
If you do not have an account yet you can create one in the browser too. Alternatively you can log in with Google or Discord.
|
||||
</TextBlock>
|
||||
|
||||
<ProgressBar IsIndeterminate="True" IsVisible="{CompiledBinding Code, Converter={x:Static StringConverters.IsNullOrEmpty}}"></ProgressBar>
|
||||
<StackPanel IsVisible="{CompiledBinding Code, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock TextWrapping="Wrap">When logging in please enter the the code below</TextBlock>
|
||||
<StackPanel Margin="0 10" Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5">
|
||||
<TextBox Width="115" IsReadOnly="True" Text="{CompiledBinding Code}" />
|
||||
<Button Width="32" Height="32" ToolTip.Tip="Copy code to clipboard" Command="{CompiledBinding CopyToClipboard}">
|
||||
<avalonia:MaterialIcon Kind="ContentCopy"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Button Width="152" HorizontalAlignment="Center" Command="{CompiledBinding OpenBrowser}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<avalonia:MaterialIcon Kind="ExternalLink" />
|
||||
<TextBlock>Open browser</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<TextBlock>Waiting for login...</TextBlock>
|
||||
<ProgressBar Margin="0 5 0 0" IsIndeterminate="True"></ProgressBar>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -1,11 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
@ -19,10 +15,8 @@ namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||
public class WorkshopLoginViewModel : ContentDialogViewModelBase
|
||||
{
|
||||
private readonly IAuthenticationService _authenticationService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly CancellationTokenSource _cts;
|
||||
private ObservableAsPropertyHelper<string?>? _deviceCode;
|
||||
private string? _uri;
|
||||
private readonly INotificationService _notificationService;
|
||||
|
||||
public WorkshopLoginViewModel(IAuthenticationService authenticationService, INotificationService notificationService)
|
||||
{
|
||||
@ -30,35 +24,20 @@ public class WorkshopLoginViewModel : ContentDialogViewModelBase
|
||||
_notificationService = notificationService;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
OpenBrowser = ReactiveCommand.Create(ExecuteOpenBrowser);
|
||||
CopyToClipboard = ReactiveCommand.CreateFromTask(ExecuteCopyToClipboard);
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(Login);
|
||||
|
||||
_deviceCode = _authenticationService.UserCode
|
||||
.Select(uc => uc != null ? string.Join("-", string.Join("-", uc.Chunk(3).Select(c => new string(c)))) : null)
|
||||
.ToProperty(this, vm => vm.Code)
|
||||
.DisposeWith(d);
|
||||
_authenticationService.VerificationUri.Subscribe(u => _uri = u).DisposeWith(d);
|
||||
Disposable.Create(_cts, cts => cts.Cancel()).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> OpenBrowser { get; }
|
||||
public ReactiveCommand<Unit, Unit> CopyToClipboard { get; }
|
||||
|
||||
public string? Code => _deviceCode?.Value;
|
||||
|
||||
private async Task Login()
|
||||
{
|
||||
try
|
||||
{
|
||||
bool result = await _authenticationService.Login(_cts.Token);
|
||||
if (result)
|
||||
_notificationService.CreateNotification().WithTitle("Login succeeded").WithSeverity(NotificationSeverity.Success).Show();
|
||||
ContentDialog?.Hide(result ? ContentDialogResult.Primary : ContentDialogResult.Secondary);
|
||||
await _authenticationService.Login(_cts.Token);
|
||||
_notificationService.CreateNotification().WithTitle("Login succeeded").WithSeverity(NotificationSeverity.Success).Show();
|
||||
ContentDialog?.Hide(ContentDialogResult.Primary);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -67,19 +46,4 @@ public class WorkshopLoginViewModel : ContentDialogViewModelBase
|
||||
ContentDialog?.Hide(ContentDialogResult.Secondary);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteOpenBrowser()
|
||||
{
|
||||
if (_uri == null)
|
||||
return;
|
||||
Utilities.OpenUrl(_uri);
|
||||
}
|
||||
|
||||
private async Task ExecuteCopyToClipboard()
|
||||
{
|
||||
if (Code == null)
|
||||
return;
|
||||
await Shared.UI.Clipboard.SetTextAsync(Code);
|
||||
_notificationService.CreateNotification().WithMessage("Code copied to clipboard.").Show();
|
||||
}
|
||||
}
|
||||
233
src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs
Normal file
233
src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs
Normal file
@ -0,0 +1,233 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Net;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Artemis.Core;
|
||||
using Artemis.WebClient.Workshop.Repositories;
|
||||
using IdentityModel;
|
||||
using IdentityModel.Client;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.Services;
|
||||
|
||||
internal class AuthenticationService : CorePropertyChanged, IAuthenticationService
|
||||
{
|
||||
private const string CLIENT_ID = "artemis.desktop";
|
||||
private readonly IAuthenticationRepository _authenticationRepository;
|
||||
private readonly SemaphoreSlim _authLock = new(1);
|
||||
private readonly ObservableCollection<Claim> _claims = new();
|
||||
|
||||
private readonly IDiscoveryCache _discoveryCache;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly BehaviorSubject<bool> _isLoggedInSubject = new(false);
|
||||
|
||||
private AuthenticationToken? _token;
|
||||
|
||||
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_discoveryCache = discoveryCache;
|
||||
_authenticationRepository = authenticationRepository;
|
||||
|
||||
Claims = new ReadOnlyObservableCollection<Claim>(_claims);
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
private static byte[] HashSha256(string inputString)
|
||||
{
|
||||
using SHA256 sha256 = SHA256.Create();
|
||||
return sha256.ComputeHash(Encoding.UTF8.GetBytes(inputString));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IObservable<bool> IsLoggedIn => _isLoggedInSubject.AsObservable();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||
|
||||
public async Task<string?> GetBearer()
|
||||
{
|
||||
await _authLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// If not logged in, attempt to auto login first
|
||||
if (!_isLoggedInSubject.Value)
|
||||
await AutoLogin();
|
||||
|
||||
if (_token == null)
|
||||
return null;
|
||||
|
||||
// If the token is expiring, refresh it
|
||||
if (_token.Expired && !await UseRefreshToken(_token.RefreshToken))
|
||||
return null;
|
||||
|
||||
return _token.AccessToken;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_authLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AutoLogin()
|
||||
{
|
||||
await _authLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (_isLoggedInSubject.Value)
|
||||
return true;
|
||||
|
||||
string? refreshToken = _authenticationRepository.GetRefreshToken();
|
||||
if (refreshToken == null)
|
||||
return false;
|
||||
|
||||
return await UseRefreshToken(refreshToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_authLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Login(CancellationToken cancellationToken)
|
||||
{
|
||||
await _authLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (_isLoggedInSubject.Value)
|
||||
return;
|
||||
|
||||
// Start a HTTP listener, this port could be in use but chances are very slim
|
||||
string redirectUri = "http://localhost:56789";
|
||||
using HttpListener listener = new();
|
||||
listener.Prefixes.Add(redirectUri + "/");
|
||||
listener.Start();
|
||||
|
||||
// Discover the Identity endpoint
|
||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||
if (disco.AuthorizeEndpoint == null)
|
||||
throw new ArtemisWebClientException("Could not determine the authorize endpoint");
|
||||
|
||||
// Generate the PKCE code verifier and code challenge
|
||||
string codeVerifier = CryptoRandom.CreateUniqueId();
|
||||
string codeChallenge = Base64Url.Encode(HashSha256(codeVerifier));
|
||||
string state = Guid.NewGuid().ToString("N");
|
||||
|
||||
// Open the web browser for the user to log in and authorize the app
|
||||
RequestUrl authRequestUrl = new(disco.AuthorizeEndpoint);
|
||||
string url = authRequestUrl.CreateAuthorizeUrl(
|
||||
CLIENT_ID,
|
||||
"code",
|
||||
"openid profile email offline_access api",
|
||||
redirectUri,
|
||||
state,
|
||||
codeChallenge: codeChallenge,
|
||||
codeChallengeMethod: OidcConstants.CodeChallengeMethods.Sha256);
|
||||
Utilities.OpenUrl(url);
|
||||
|
||||
// Wait for the callback with the code
|
||||
HttpListenerContext context = await listener.GetContextAsync().WaitAsync(cancellationToken);
|
||||
string? code = context.Request.QueryString.Get("code");
|
||||
string? returnState = context.Request.QueryString.Get("state");
|
||||
|
||||
// Validate that a code was given and that our state matches, ensuring we don't respond to a request we did not initialize
|
||||
if (code == null || returnState != state)
|
||||
throw new ArtemisWebClientException("Did not get the expected response on the callback");
|
||||
|
||||
// Redirect the browser to a page hosted by Identity indicating success
|
||||
context.Response.StatusCode = (int) HttpStatusCode.Redirect;
|
||||
context.Response.AddHeader("Location", $"{WorkshopConstants.AUTHORITY_URL}/account/login/success");
|
||||
context.Response.Close();
|
||||
|
||||
// Request auth tokens
|
||||
HttpClient client = _httpClientFactory.CreateClient();
|
||||
TokenResponse response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
|
||||
{
|
||||
Address = disco.TokenEndpoint,
|
||||
ClientId = CLIENT_ID,
|
||||
Code = code,
|
||||
CodeVerifier = codeVerifier,
|
||||
RedirectUri = redirectUri
|
||||
}, cancellationToken);
|
||||
|
||||
if (response.IsError)
|
||||
throw new ArtemisWebClientException("Failed to request device authorization: " + response.Error);
|
||||
|
||||
// Set the current user using the new tokens
|
||||
SetCurrentUser(response);
|
||||
|
||||
listener.Stop();
|
||||
listener.Close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_authLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Logout()
|
||||
{
|
||||
_token = null;
|
||||
_claims.Clear();
|
||||
_authenticationRepository.SetRefreshToken(null);
|
||||
|
||||
_isLoggedInSubject.OnNext(false);
|
||||
}
|
||||
}
|
||||
@ -1,222 +1,16 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Security.Claims;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.WebClient.Workshop.Repositories;
|
||||
using IdentityModel;
|
||||
using IdentityModel.Client;
|
||||
|
||||
namespace Artemis.WebClient.Workshop.Services;
|
||||
|
||||
public interface IAuthenticationService : IProtectedArtemisService
|
||||
{
|
||||
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(CancellationToken cancellationToken);
|
||||
Task 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 IHttpClientFactory _httpClientFactory;
|
||||
private readonly BehaviorSubject<bool> _isLoggedInSubject = new(false);
|
||||
private readonly BehaviorSubject<string?> _userCodeSubject = new(null);
|
||||
private readonly BehaviorSubject<string?> _verificationUriSubject = new(null);
|
||||
|
||||
private AuthenticationToken? _token;
|
||||
|
||||
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_discoveryCache = discoveryCache;
|
||||
_authenticationRepository = authenticationRepository;
|
||||
|
||||
Claims = new ReadOnlyObservableCollection<Claim>(_claims);
|
||||
}
|
||||
|
||||
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, 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 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; }
|
||||
|
||||
public async Task<string?> GetBearer()
|
||||
{
|
||||
await _bearerLock.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// If not logged in, attempt to auto login first
|
||||
if (!_isLoggedInSubject.Value)
|
||||
await AutoLogin();
|
||||
|
||||
if (_token == null)
|
||||
return null;
|
||||
|
||||
// If the token is expiring, refresh it
|
||||
if (_token.Expired && !await UseRefreshToken(_token.RefreshToken))
|
||||
return null;
|
||||
|
||||
return _token.AccessToken;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_bearerLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> AutoLogin()
|
||||
{
|
||||
if (_isLoggedInSubject.Value)
|
||||
return true;
|
||||
|
||||
string? refreshToken = _authenticationRepository.GetRefreshToken();
|
||||
if (refreshToken == null)
|
||||
return false;
|
||||
|
||||
return await UseRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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,
|
||||
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);
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval), cancellationToken);
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_userCodeSubject.OnNext(null);
|
||||
_verificationUriSubject.OnNext(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Logout()
|
||||
{
|
||||
_token = null;
|
||||
_claims.Clear();
|
||||
_authenticationRepository.SetRefreshToken(null);
|
||||
|
||||
_isLoggedInSubject.OnNext(false);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user