Skip to content

Commit

Permalink
Merge pull request #44 from /issues/25-LoginBadRequestHandling
Browse files Browse the repository at this point in the history
  • Loading branch information
tpill90 committed Aug 9, 2023
2 parents 7bf28b6 + 59431fc commit 01abeb2
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ env:

jobs:
build-test:
uses: tpill90/lancache-prefill-common/.github/workflows/build-and-test.yml@main
uses: tpill90/lancache-prefill-common/.github/workflows/build-and-test-template.yml@main
with:
PROJECT_NAME: EpicPrefill
2 changes: 1 addition & 1 deletion EpicPrefill/Handlers/HttpClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public HttpClientFactory(IAnsiConsole ansiConsole, UserAccountManager userAccoun
//TODO document
public async Task<HttpClient> GetHttpClientAsync()
{
if (_userAccountManager.IsOauthTokenExpired())
if (_userAccountManager.OauthTokenIsExpired())
{
await _userAccountManager.LoginAsync();
}
Expand Down
133 changes: 88 additions & 45 deletions EpicPrefill/Handlers/UserAccountManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace EpicPrefill.Handlers
{
//TODO document
//TODO should probably have a private default constructor, so you have to use the Load() method
/// <summary>
/// https://dev.epicgames.com/docs/web-api-ref/authentication
/// </summary>
Expand All @@ -10,19 +9,21 @@
public sealed class UserAccountManager
{
private readonly IAnsiConsole _ansiConsole;
private readonly HttpClient _client;

//TODO comment properties
private HttpClient _client;
private string loginUrl = "https://legendary.gl/epiclogin";
private string _oauth_host = "account-public-service-prod03.ol.epicgames.com";
//TODO I'm not sure where this link comes from. Can I possibly setup my own?
private const string LoginUrl = "https://legendary.gl/epiclogin";
private const string OauthHost = "account-public-service-prod03.ol.epicgames.com";

private string _user_basic = "34a02cf8f4414e29b15921876da36f9a";
private string _pw_basic = "daafbccc737745039dffe53d94fc76cf";
private const string BasicUsername = "34a02cf8f4414e29b15921876da36f9a";
private const string BasicPassword = "daafbccc737745039dffe53d94fc76cf";

private const int MaxRetries = 3;

//TODO this should probably be private
public OauthToken OauthToken { get; set; }

public UserAccountManager(IAnsiConsole ansiConsole)
private UserAccountManager(IAnsiConsole ansiConsole)
{
_ansiConsole = ansiConsole;
_client = new HttpClient
Expand All @@ -32,60 +33,109 @@ public UserAccountManager(IAnsiConsole ansiConsole)
_client.DefaultRequestHeaders.Add("User-Agent", AppConfig.DefaultUserAgent);
}

//TODO document
public async Task LoginAsync()
{
if (OauthToken != null && !IsOauthTokenExpired())
if (!OauthTokenIsExpired())
{
_ansiConsole.LogMarkupLine("Reusing existing auth session...");
return;
}

var requestParams = new Dictionary<string, string>
int retryCount = 0;
while (OauthTokenIsExpired() && retryCount < MaxRetries)
{
{ "token_type", "eg1" }
};
try
{
var requestParams = BuildRequestParams();

// Handles the user logging in for the first time
if (OauthToken == null)
var authUri = new Uri($"https://{OauthHost}/account/api/oauth/token");
using var request = new HttpRequestMessage(HttpMethod.Post, authUri);
request.Headers.Authorization = BasicAuthentication.ToAuthenticationHeader(BasicUsername, BasicPassword);
request.Content = new FormUrlEncodedContent(requestParams);

using var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

using var responseStream = await response.Content.ReadAsStreamAsync();
OauthToken = JsonSerializer.Deserialize(responseStream, SerializationContext.Default.OauthToken);

Save();
}
catch (Exception e)
{
// If the login failed due to a bad request then we'll clear out the existing token and try again
if (e is HttpRequestException)
{
OauthToken = null;
}
}

retryCount++;
}

if (retryCount == 3)
{
throw new EpicLoginException("Unable to login to Epic! Try again in a few moments...");
}
}

private Dictionary<string, string> BuildRequestParams()
{
// Handles the user logging in for the first time, as well as when the refresh token has expired, or when an unknown failure has occurred
if (OauthToken == null || RefreshTokenIsExpired())
{
if (RefreshTokenIsExpired())
{
_ansiConsole.LogMarkupLine(LightYellow("Refresh token has expired! EpicPrefill will need to login again..."));
}

_ansiConsole.LogMarkupLine("Please login into Epic via your browser");
//TODO which color to use for link?
_ansiConsole.LogMarkupLine($"If the web page did not open automatically, please manually open the following URL: {Cyan(loginUrl)}");
OpenUrl(loginUrl);
_ansiConsole.LogMarkupLine($"If the web page did not open automatically, please manually open the following URL: {Cyan(LoginUrl)}");
OpenUrl(LoginUrl);

//TODO handle users pasting in the entire JSON response. Or figure out a way to not require doing this at all.
//TODO might be able to do username + password without browser : https://gist.github.com/iXyles/ec40cb40a2a186425ec6bfb9dcc2ddda
var authCode = _ansiConsole.Prompt(new TextPrompt<string>($"Please enter the {LightYellow("authorizationCode")} from the JSON response:"));

requestParams.Add("grant_type", "authorization_code");
requestParams.Add("code", authCode);
return new Dictionary<string, string>
{
{ "token_type", "eg1" },
{ "grant_type", "authorization_code" },
{ "code", authCode }
};
}

// Handles a user being logged in, but the saved token has expired
if (OauthToken != null && IsOauthTokenExpired())
_ansiConsole.LogMarkupLine("Auth token expired. Requesting refresh auth token...");
return new Dictionary<string, string>
{
_ansiConsole.LogMarkupLine("Auth token expired. Requesting refresh auth token...");
{ "token_type", "eg1" },
{ "grant_type", "refresh_token" },
{ "refresh_token", OauthToken.RefreshToken }
};
}

requestParams.Add("grant_type", "refresh_token");
requestParams.Add("refresh_token", OauthToken.RefreshToken);
//TODO this should probably not be referenced externally
public bool OauthTokenIsExpired()
{
if (OauthToken == null)
{
return true;
}

// Tokens are valid for 8 hours, but we're adding a buffer of 10 minutes to make sure that the token doesn't expire while we're using it.
return DateTimeOffset.UtcNow.DateTime > OauthToken.ExpiresAt.AddMinutes(-10);
}

var authUri = new Uri($"https://{_oauth_host}/account/api/oauth/token");
using var requestMessage = new HttpRequestMessage(HttpMethod.Post, authUri);
requestMessage.Content = new FormUrlEncodedContent(requestParams);

var authenticationString = $"{_user_basic}:{_pw_basic}";
var base64EncodedAuthenticationString = System.Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(authenticationString));
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString);

using var response = await _client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
private bool RefreshTokenIsExpired()
{
if (OauthToken == null)
{
return true;
}

using var responseStream = await response.Content.ReadAsStreamAsync();
OauthToken = JsonSerializer.Deserialize(responseStream, SerializationContext.Default.OauthToken);
Save();
// Tokens are valid for 8 hours, but we're adding a buffer of 10 minutes to make sure that the token doesn't expire while we're using it.
return DateTimeOffset.UtcNow.DateTime > OauthToken.RefreshTokenExpiresAt;
}

//TODO document
Expand All @@ -103,7 +153,7 @@ public static UserAccountManager LoadFromFile(IAnsiConsole ansiConsole)
return accountManager;
}

public void Save()
private void Save()
{
using var fileStream = File.Open(AppConfig.AccountSettingsStorePath, FileMode.Create, FileAccess.Write);
JsonSerializer.Serialize(fileStream, OauthToken, SerializationContext.Default.OauthToken);
Expand Down Expand Up @@ -134,12 +184,5 @@ private void OpenUrl(string url)
Process.Start("open", url);
}
}

//TODO consider adding a buffer of 10ish minutes to the expired time, so that if a token expires while we're making requests, the requests won't fail
public bool IsOauthTokenExpired()
{
// Tokens are valid for 8 hours
return DateTimeOffset.UtcNow.DateTime > OauthToken.ExpiresAt;
}
}
}
2 changes: 1 addition & 1 deletion EpicPrefill/Models/ApiResponses/OauthToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class OauthToken
public string RefreshToken { get; set; }

[JsonPropertyName("refresh_expires_at")]
public DateTime RefreshExpiresAt { get; set; }
public DateTime RefreshTokenExpiresAt { get; set; }

[JsonPropertyName("account_id")]
public string AccountId { get; set; }
Expand Down
15 changes: 15 additions & 0 deletions EpicPrefill/Models/BasicAuthentication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace EpicPrefill.Models
{
//TODO document
public static class BasicAuthentication
{
public static AuthenticationHeaderValue ToAuthenticationHeader(string username, string password)
{
string authenticationString = $"{username}:{password}";
byte[] inArray = Encoding.ASCII.GetBytes(authenticationString);
var base64EncodedAuthenticationString = System.Convert.ToBase64String(inArray);

return new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString);
}
}
}
25 changes: 25 additions & 0 deletions EpicPrefill/Models/Exceptions/EpicLoginException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace EpicPrefill.Models.Exceptions
{
public class EpicLoginException : Exception
{
protected EpicLoginException(SerializationInfo info, StreamingContext context) : base(info, context)
{

}

public EpicLoginException()
{

}

public EpicLoginException(string message) : base(message)
{

}

public EpicLoginException(string message, Exception inner) : base(message, inner)
{

}
}
}
2 changes: 1 addition & 1 deletion EpicPrefill/Properties/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
global using Terminal.Gui;
global using static LancachePrefill.Common.SpectreFormatters;
global using AnsiConsoleExtensions = LancachePrefill.Common.Extensions.AnsiConsoleExtensions;
global using System.Threading;
global using System.Runtime.Serialization;
global using EpicPrefill.Extensions;
global using EpicPrefill.Models.Manifests;
global using EpicPrefill.Models.Exceptions;

0 comments on commit 01abeb2

Please sign in to comment.