Skip to content

Commit

Permalink
Refactored authentication steps
Browse files Browse the repository at this point in the history
  • Loading branch information
leMicin committed Nov 16, 2023
1 parent a65322b commit a5da0d3
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 198 deletions.
211 changes: 126 additions & 85 deletions src/Sidekick.Apis.Poe/Authentication/AuthenticationService.cs
Original file line number Diff line number Diff line change
@@ -1,128 +1,183 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Sidekick.Apis.Poe.Authentication.Models;
using Sidekick.Common.Browser;
using Sidekick.Common.Platform.Interprocess;
using Sidekick.Common.Settings;

namespace Sidekick.Apis.Poe.Authentication
{
internal class AuthenticationService : IAuthenticationService
internal class AuthenticationService : IAuthenticationService, IDisposable
{
private const string REDIRECTURL = "https://sidekick-poe.github.io/oauth/poe";
private const string AUTHORIZATIONURL = "https://www.pathofexile.com/oauth/authorize";
private const string TOKENURL = "https://www.pathofexile.com/oauth/token";
private const string REDIRECTURL = "https://sidekick-poe.github.io/oauth/poe";
private const string CLIENTID = "sidekick";
private const string SCOPES = "account:stashes";
private const string TOKENURL = "https://www.pathofexile.com/oauth/token";

private static string _code { get; set; }
private static string _state { get; set; }
private static string _verifier { get; set; }
private static string _challenge { get; set; }
private static string _token { get; set; }
private static bool _isAuthenticating { get; set; }
private readonly ISettings settings;
private readonly ISettingsService settingsService;
private readonly IBrowserProvider browserProvider;
private readonly IInterprocessService interprocessService;
private readonly HttpClient client;

private ISettings _settings { get; set; }
private ISettingsService _settingsService { get; set; }
private IBrowserProvider _browser { get; set; }
private HttpClient _client { get; set; }
public event Action? OnStateChanged;

public AuthenticationService(
ISettings settings,
ISettingsService settingsService,
IBrowserProvider browser,
IHttpClientFactory clientFactory)
IBrowserProvider browserProvider,
IHttpClientFactory clientFactory,
IInterprocessService interprocessService)
{
_settings = settings;
_settingsService = settingsService;
_browser = browser;
_client = clientFactory.CreateClient();
_isAuthenticating = false;
this.settings = settings;
this.settingsService = settingsService;
this.browserProvider = browserProvider;
this.interprocessService = interprocessService;

client = clientFactory.CreateClient();
interprocessService.OnMessageReceived += InterprocessService_CustomProtocolCallback;
}

public Task Authenticate()
private string? State { get; set; }
private string? Verifier { get; set; }
private string? Challenge { get; set; }
private TaskCompletionSource? AuthenticateTask { get; set; }
private CancellationTokenSource? AuthenticateTokenSource { get; set; }

public AuthenticationState CurrentState
{
if (!_isAuthenticating)
get
{
_isAuthenticating = true;
_state = Guid.NewGuid().ToString();
_verifier = GenerateCodeVerifier();
_challenge = GenerateCodeChallenge();
_browser.OpenUri(new Uri(GenerateUserLink()));
if(AuthenticateTask != null && AuthenticateTask.Task.Status == TaskStatus.Running && AuthenticateTokenSource != null && !AuthenticateTokenSource.IsCancellationRequested)
{
return AuthenticationState.InProgress;
}

if (settings.Bearer_Expiration == null || string.IsNullOrEmpty(settings.Bearer_Token))
{
return AuthenticationState.Unauthenticated;
}

if (DateTimeOffset.Now < settings.Bearer_Expiration)
{
return AuthenticationState.Authenticated;
}

return AuthenticationState.Unauthenticated;
}
return Task.CompletedTask;
}

public async Task<string> AuthenticationCallback(string code, string state)
public string? GetToken()
{
if (_state == state)
OnStateChanged?.Invoke();

if (CurrentState == AuthenticationState.Authenticated)
{
_code = code;
_token = await RequestAccessToken();
return settings.Bearer_Token;
}
return _token;

return null;
}

public string GetAccessToken()
public Task Authenticate()
{
if (!IsAuthenticated())
if (CurrentState == AuthenticationState.Authenticated)
{
return string.Empty;
return Task.CompletedTask;
}

return _settings.Bearer_Token;
if (CurrentState == AuthenticationState.InProgress)
{
return AuthenticateTask!.Task;
}

State = Guid.NewGuid().ToString();
Verifier = GenerateCodeVerifier();
Challenge = GenerateCodeChallenge(Verifier);

var authenticationLink = $"{AUTHORIZATIONURL}?client_id={CLIENTID}&response_type=code&scope={SCOPES}&state={State}&redirect_uri={REDIRECTURL}&code_challenge={Challenge}&code_challenge_method=S256";
browserProvider.OpenUri(new Uri(authenticationLink));

AuthenticateTask = new();
AuthenticateTokenSource = new(30000);
OnStateChanged?.Invoke();

return AuthenticateTask.Task;
}

public bool IsAuthenticated()
private void CancelAuthenticate()
{
if (_isAuthenticating)
if (AuthenticateTask != null)
{
return false;
AuthenticateTask.SetResult();
AuthenticateTask = null;
}

if (_settings.Bearer_Expiration == null || String.IsNullOrEmpty(_settings.Bearer_Token))
if (AuthenticateTokenSource != null)
{
return false;
AuthenticateTokenSource.Cancel();
AuthenticateTokenSource = null;
}

if (_settings.Bearer_Expiration?.AddMinutes(-1) < DateTime.Now)
OnStateChanged?.Invoke();
}

private void InterprocessService_CustomProtocolCallback(string message)
{
if (!message.ToUpper().StartsWith("SIDEKICK://OAUTH/POE"))
{
_settingsService.Save(nameof(Settings.Bearer_Token), null);
_settingsService.Save(nameof(Settings.Bearer_Expiration), null);
return false;
return;
}

return true;
}
var queryDictionary = System.Web.HttpUtility.ParseQueryString(new Uri(message).Query);
var state = queryDictionary["state"];
var code = queryDictionary["code"];

public bool IsAuthenticating()
{
return _isAuthenticating;
if (string.IsNullOrEmpty(state) || string.IsNullOrEmpty(code))
{
CancelAuthenticate();
return;
}

_ = RequestAccessToken(state, code);
}

private async Task<string> RequestAccessToken()
private async Task RequestAccessToken(string state, string code)
{
if (state != State)
{
CancelAuthenticate();
return;
}

var requestContent = new StringContent(
$"client_id={CLIENTID}&grant_type=authorization_code&code={_code}&redirect_uri={REDIRECTURL}&scope={SCOPES}&code_verifier={_verifier}",
$"client_id={CLIENTID}&grant_type=authorization_code&code={code}&redirect_uri={REDIRECTURL}&scope={SCOPES}&code_verifier={Verifier}",
Encoding.UTF8,
"application/x-www-form-urlencoded"
);

var response = await _client.PostAsync(TOKENURL, requestContent);
// var responseString = await response.Content.ReadAsStringAsync();
var response = await client.PostAsync(TOKENURL, requestContent);
var responseContent = await response.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync<Oauth2TokenResponse>(responseContent);

await _settingsService.Save(nameof(Settings.Bearer_Token), result.access_token);
await _settingsService.Save(nameof(Settings.Bearer_Expiration), DateTime.Now.AddSeconds(result.expires_in));
if(result == null || result.access_token == null)
{
CancelAuthenticate();
return;
}

_isAuthenticating = false;
await settingsService.Save(nameof(Settings.Bearer_Token), result.access_token);
await settingsService.Save(nameof(Settings.Bearer_Expiration), DateTime.Now.AddSeconds(result.expires_in));

return result.access_token;
if (AuthenticateTask != null)
{
AuthenticateTask.SetResult();
}
}

private string GenerateCodeVerifier()
private static string GenerateCodeVerifier()
{
//Generate a random string for our code verifier
var rng = RandomNumberGenerator.Create();
var bytes = new byte[32];
rng.GetBytes(bytes);
Expand All @@ -134,36 +189,22 @@ private string GenerateCodeVerifier()
return codeVerifier;
}

private string GenerateCodeChallenge()
private static string GenerateCodeChallenge(string verifier)
{
//generate the code challenge based on the verifier
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(_verifier));
codeChallenge = Convert.ToBase64String(challengeBytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
using var sha256 = SHA256.Create();
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(verifier));
codeChallenge = Convert.ToBase64String(challengeBytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');

return codeChallenge;
}

private string GenerateUserLink()
{
return $"{AUTHORIZATIONURL}?client_id={CLIENTID}&response_type=code&scope={SCOPES}&state={_state}&redirect_uri={REDIRECTURL}&code_challenge={_challenge}&code_challenge_method=S256";
}

private class Oauth2TokenResponse
public void Dispose()
{
public string access_token { get; set; }
public int expires_in { get; set; }
public string token_type { get; set; }
public string scope { get; set; }
public string username { get; set; }
public string sub { get; set; }
public string refresh_token { get; set; }
interprocessService.OnMessageReceived -= InterprocessService_CustomProtocolCallback;
}
}
}
9 changes: 9 additions & 0 deletions src/Sidekick.Apis.Poe/Authentication/AuthenticationState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Sidekick.Apis.Poe.Authentication
{
public enum AuthenticationState
{
Unauthenticated,
InProgress,
Authenticated,
}
}
13 changes: 4 additions & 9 deletions src/Sidekick.Apis.Poe/Authentication/IAuthenticationService.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
using Sidekick.Apis.Poe.Trade.Models;
using Sidekick.Common.Game.Items;

namespace Sidekick.Apis.Poe.Authentication
{
public interface IAuthenticationService
{
Task Authenticate();
event Action? OnStateChanged;

Task<string> AuthenticationCallback(string auth, string state);
AuthenticationState CurrentState { get; }

string GetAccessToken();
string? GetToken();

bool IsAuthenticated();

bool IsAuthenticating();
Task Authenticate();
}
}
13 changes: 13 additions & 0 deletions src/Sidekick.Apis.Poe/Authentication/Models/Oauth2TokenResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Sidekick.Apis.Poe.Authentication.Models
{
internal class Oauth2TokenResponse
{
public string? access_token { get; set; }
public int expires_in { get; set; }
public string? token_type { get; set; }
public string? scope { get; set; }
public string? username { get; set; }
public string? sub { get; set; }
public string? refresh_token { get; set; }
}
}
11 changes: 4 additions & 7 deletions src/Sidekick.Apis.Poe/Clients/PoeApiHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Net.Http.Headers;
using ComposableAsync;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -33,19 +32,17 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
{
await timeConstraint;

var token = authenticationService.GetAccessToken();

if (String.IsNullOrEmpty(token))
var token = authenticationService.GetToken();
if (string.IsNullOrEmpty(token))
{
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}

request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

var response = await base.SendAsync(request, cancellationToken);
var response = await base.SendAsync(request, cancellationToken);

return response;

}
}
}
1 change: 1 addition & 0 deletions src/Sidekick.Apis.Poe/Sidekick.Apis.Poe.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Sidekick.Common.Platform\Sidekick.Common.Platform.csproj" />
<ProjectReference Include="..\Sidekick.Common\Sidekick.Common.csproj" />
</ItemGroup>

Expand Down
Loading

0 comments on commit a5da0d3

Please sign in to comment.