v1.3.0.1 (Epic Store release)
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
public sealed class EgsIdTokenPrivate : EosInterface.EgsIdToken
|
||||
{
|
||||
public override EpicAccountId AccountId { get; }
|
||||
|
||||
internal static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
IncludeFields = true
|
||||
};
|
||||
|
||||
internal readonly record struct TokenStruct(
|
||||
string AccountId,
|
||||
string JsonWebToken);
|
||||
|
||||
internal readonly Epic.OnlineServices.Auth.IdToken InternalToken;
|
||||
internal EgsIdTokenPrivate(EpicAccountId accountId, Epic.OnlineServices.Auth.IdToken internalToken)
|
||||
{
|
||||
AccountId = accountId;
|
||||
InternalToken = internalToken;
|
||||
}
|
||||
|
||||
public new static Option<EgsIdTokenPrivate> Parse(string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (JsonSerializer.Deserialize(
|
||||
str,
|
||||
returnType: typeof(TokenStruct),
|
||||
options: JsonSerializerOptions)
|
||||
is not TokenStruct tokenStruct)
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
if (!EpicAccountId.Parse(tokenStruct.AccountId).TryUnwrap(out var accountId)) { return Option.None; }
|
||||
|
||||
var internalToken = new Epic.OnlineServices.Auth.IdToken
|
||||
{
|
||||
AccountId = Epic.OnlineServices.EpicAccountId.FromString(tokenStruct.AccountId),
|
||||
JsonWebToken = tokenStruct.JsonWebToken
|
||||
};
|
||||
|
||||
return Option.Some(new EgsIdTokenPrivate(accountId, internalToken));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Option.None;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var tokenStruct = new TokenStruct(
|
||||
AccountId: InternalToken.AccountId.ToString(),
|
||||
JsonWebToken: InternalToken.JsonWebToken);
|
||||
return JsonSerializer.Serialize(tokenStruct, options: JsonSerializerOptions);
|
||||
}
|
||||
|
||||
public static Result<EosInterface.EgsIdToken, EosInterface.GetEgsSelfIdTokenError> GetEgsIdTokenForEpicAccountId(EpicAccountId accountId)
|
||||
{
|
||||
var (success, failure) = Result<EosInterface.EgsIdToken, EosInterface.GetEgsSelfIdTokenError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return failure(EosInterface.GetEgsSelfIdTokenError.EosNotInitialized); }
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions
|
||||
{
|
||||
AccountId = Epic.OnlineServices.EpicAccountId.FromString(accountId.EosStringRepresentation)
|
||||
};
|
||||
var copyIdTokenResult = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var idTokenNullable);
|
||||
|
||||
if (copyIdTokenResult is Epic.OnlineServices.Result.NotFound) { return failure(EosInterface.GetEgsSelfIdTokenError.InvalidToken); }
|
||||
if (copyIdTokenResult != Epic.OnlineServices.Result.Success) { return failure(EosInterface.GetEgsSelfIdTokenError.UnhandledErrorCondition); }
|
||||
if (idTokenNullable is not { } idToken) { return failure(EosInterface.GetEgsSelfIdTokenError.UnhandledErrorCondition); }
|
||||
|
||||
return success(new EgsIdTokenPrivate(accountId, idToken));
|
||||
}
|
||||
|
||||
public override async Task<EosInterface.VerifyEgsIdTokenResult> Verify(AccountId accountId)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return EosInterface.VerifyEgsIdTokenResult.Failed; }
|
||||
|
||||
var verifyIdTokenOptions = new Epic.OnlineServices.Auth.VerifyIdTokenOptions
|
||||
{
|
||||
IdToken = InternalToken
|
||||
};
|
||||
var verifyIdTokenWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.VerifyIdTokenCallbackInfo>();
|
||||
egsAuthInterface.VerifyIdToken(options: ref verifyIdTokenOptions, clientData: null, completionDelegate: verifyIdTokenWaiter.OnCompletion);
|
||||
var result = await verifyIdTokenWaiter.Task;
|
||||
if (!result.TryUnwrap(out var callbackInfo)) { return EosInterface.VerifyEgsIdTokenResult.Failed; }
|
||||
|
||||
if (callbackInfo.ResultCode != Epic.OnlineServices.Result.Success
|
||||
|| callbackInfo.ProductId != CorePrivate.PlatformInterfaceOptions.ProductId)
|
||||
{
|
||||
return EosInterface.VerifyEgsIdTokenResult.Failed;
|
||||
}
|
||||
|
||||
var resultAccountId = IdQueriesPrivate.EosStringToAccountId(callbackInfo.ExternalAccountId, callbackInfo.ExternalAccountIdType);
|
||||
|
||||
return resultAccountId.TryUnwrap(out var resultId) && resultId == accountId
|
||||
? EosInterface.VerifyEgsIdTokenResult.Verified
|
||||
: EosInterface.VerifyEgsIdTokenResult.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Option<EosInterface.EgsIdToken> ParseEgsIdToken(string str)
|
||||
=> EgsIdTokenPrivate.Parse(str).Select(t => (EosInterface.EgsIdToken)t);
|
||||
|
||||
public override Result<EosInterface.EgsIdToken, EosInterface.GetEgsSelfIdTokenError> GetEgsIdTokenForEpicAccountId(EpicAccountId accountId)
|
||||
=> EgsIdTokenPrivate.GetEgsIdTokenForEpicAccountId(accountId);
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class EosIdTokenPrivate
|
||||
{
|
||||
public static Result<EosInterface.EosIdToken, EosInterface.GetEosSelfIdTokenError> GetEosIdTokenForProductUserId(EosInterface.ProductUserId puid)
|
||||
{
|
||||
var (success, failure) = Result<EosInterface.EosIdToken, EosInterface.GetEosSelfIdTokenError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return failure(EosInterface.GetEosSelfIdTokenError.EosNotInitialized); }
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Connect.CopyIdTokenOptions
|
||||
{
|
||||
LocalUserId = Epic.OnlineServices.ProductUserId.FromString(puid.Value)
|
||||
};
|
||||
var copyIdTokenResult = connectInterface.CopyIdToken(ref copyIdTokenOptions, out var idTokenNullable);
|
||||
|
||||
if (copyIdTokenResult is Epic.OnlineServices.Result.NotFound) { return failure(EosInterface.GetEosSelfIdTokenError.InvalidToken); }
|
||||
if (copyIdTokenResult != Epic.OnlineServices.Result.Success) { return failure(EosInterface.GetEosSelfIdTokenError.UnhandledErrorCondition); }
|
||||
if (idTokenNullable is not { } idToken) { return failure(EosInterface.GetEosSelfIdTokenError.UnhandledErrorCondition); }
|
||||
|
||||
if (!JsonWebToken.Parse(idToken.JsonWebToken).TryUnwrap(out var jsonWebToken)) { return failure(EosInterface.GetEosSelfIdTokenError.CouldNotParseJwt); }
|
||||
|
||||
return success(new EosInterface.EosIdToken(new EosInterface.ProductUserId(idToken.ProductUserId.ToString()), jsonWebToken));
|
||||
}
|
||||
|
||||
public static async Task<Result<AccountId, EosInterface.VerifyEosIdTokenError>> Verify(EosInterface.EosIdToken token)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.VerifyEosIdTokenError.EosNotInitialized); }
|
||||
|
||||
var verifyIdTokenOptions = new Epic.OnlineServices.Connect.VerifyIdTokenOptions
|
||||
{
|
||||
IdToken = new Epic.OnlineServices.Connect.IdToken
|
||||
{
|
||||
ProductUserId = Epic.OnlineServices.ProductUserId.FromString(token.ProductUserId.Value),
|
||||
JsonWebToken = token.JsonWebToken.ToString()
|
||||
}
|
||||
};
|
||||
var verifyIdTokenWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.VerifyIdTokenCallbackInfo>();
|
||||
connectInterface.VerifyIdToken(options: ref verifyIdTokenOptions, clientData: null, completionDelegate: verifyIdTokenWaiter.OnCompletion);
|
||||
var result = await verifyIdTokenWaiter.Task;
|
||||
if (!result.TryUnwrap(out var callbackInfo)) { return Result.Failure(EosInterface.VerifyEosIdTokenError.TimedOut); }
|
||||
|
||||
if (callbackInfo.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(EosInterface.VerifyEosIdTokenError.UnhandledErrorCondition);
|
||||
}
|
||||
|
||||
if (callbackInfo.ProductId != CorePrivate.PlatformInterfaceOptions.ProductId)
|
||||
{
|
||||
return Result.Failure(EosInterface.VerifyEosIdTokenError.ProductIdDidNotMatch);
|
||||
}
|
||||
|
||||
var resultAccountId = IdQueriesPrivate.EosStringToAccountId(callbackInfo.AccountId, callbackInfo.AccountIdType);
|
||||
|
||||
return resultAccountId.TryUnwrap(out var resultId)
|
||||
? Result.Success(resultId)
|
||||
: Result.Failure(EosInterface.VerifyEosIdTokenError.CouldNotParseExternalAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<AccountId, EosInterface.VerifyEosIdTokenError>> VerifyEosIdToken(EosInterface.EosIdToken token)
|
||||
=> TaskScheduler.Schedule(() => EosIdTokenPrivate.Verify(token));
|
||||
|
||||
public override Result<EosInterface.EosIdToken, EosInterface.GetEosSelfIdTokenError> GetEosIdTokenForProductUserId(EosInterface.ProductUserId puid)
|
||||
=> EosIdTokenPrivate.GetEosIdTokenForProductUserId(puid);
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class IdQueriesPrivate
|
||||
{
|
||||
public static ImmutableArray<EosInterface.ProductUserId> GetLoggedInPuids()
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return ImmutableArray<EosInterface.ProductUserId>.Empty; }
|
||||
|
||||
int count = connectInterface.GetLoggedInUsersCount();
|
||||
var ids = new List<EosInterface.ProductUserId>();
|
||||
foreach (int i in Enumerable.Range(0, count))
|
||||
{
|
||||
if (connectInterface.GetLoggedInUserByIndex(i) is not { } userId) { return ImmutableArray<EosInterface.ProductUserId>.Empty; }
|
||||
var newPuid = new EosInterface.ProductUserId(userId.ToString());
|
||||
if (!LoginPrivate.PuidToPrimaryExternalId.ContainsKey(newPuid)) { continue; }
|
||||
ids.Add(newPuid);
|
||||
}
|
||||
|
||||
return ids.ToImmutableArray();
|
||||
}
|
||||
|
||||
public static ImmutableArray<EpicAccountId> GetLoggedInEpicIds()
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return ImmutableArray<EpicAccountId>.Empty; }
|
||||
|
||||
int count = egsAuthInterface.GetLoggedInAccountsCount();
|
||||
var ids = new List<EpicAccountId>();
|
||||
foreach (int i in Enumerable.Range(0, count))
|
||||
{
|
||||
if (egsAuthInterface.GetLoggedInAccountByIndex(i) is not { } userId) { return ImmutableArray<EpicAccountId>.Empty; }
|
||||
var newEpicIdOption = EpicAccountId.Parse(userId.ToString());
|
||||
if (!newEpicIdOption.TryUnwrap(out var newEpicId)) { return ImmutableArray<EpicAccountId>.Empty; }
|
||||
ids.Add(newEpicId);
|
||||
}
|
||||
|
||||
return ids.ToImmutableArray();
|
||||
}
|
||||
|
||||
public static Task<Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>>
|
||||
GetSelfExternalAccountIds(
|
||||
EosInterface.ProductUserId productUserId)
|
||||
=> GetExternalAccountIds(productUserId, productUserId);
|
||||
|
||||
internal static async Task<Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>>
|
||||
GetExternalAccountIds(
|
||||
EosInterface.ProductUserId selfPuid,
|
||||
EosInterface.ProductUserId puidToGetIdsFor)
|
||||
{
|
||||
// If logged only into an Epic account, you cannot fetch SteamIDs.
|
||||
// See Epic.OnlineServices.Connect.ExternalAccountInfo.AccountId
|
||||
|
||||
var (success, failure) = Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>.GetFactoryMethods();
|
||||
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface)
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.EosNotInitialized);
|
||||
}
|
||||
if (!LoginPrivate.PuidToPrimaryExternalId.ContainsKey(selfPuid))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.Inaccessible);
|
||||
}
|
||||
|
||||
var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value);
|
||||
var otherPuidInternal = Epic.OnlineServices.ProductUserId.FromString(puidToGetIdsFor.Value);
|
||||
|
||||
var queryProductUserIdMappingsOptions = new Epic.OnlineServices.Connect.QueryProductUserIdMappingsOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
ProductUserIds = new[] { otherPuidInternal }
|
||||
};
|
||||
|
||||
var queryWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.QueryProductUserIdMappingsCallbackInfo>();
|
||||
connectInterface.QueryProductUserIdMappings(options: ref queryProductUserIdMappingsOptions, clientData: null, completionDelegate: queryWaiter.OnCompletion);
|
||||
var queryResultOption = await queryWaiter.Task;
|
||||
if (!queryResultOption.TryUnwrap(out var queryResult))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.Timeout);
|
||||
}
|
||||
|
||||
if (queryResult.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return failure(queryResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.NotFound => EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser,
|
||||
Epic.OnlineServices.Result.InvalidUser => EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser,
|
||||
var unhandled => unhandled.FailAndLogUnhandledError(EosInterface.IdQueries.GetSelfExternalIdError.UnhandledErrorCondition)
|
||||
});
|
||||
}
|
||||
|
||||
var getProductUserExternalAccountCountOptions = new Epic.OnlineServices.Connect.GetProductUserExternalAccountCountOptions
|
||||
{
|
||||
TargetUserId = otherPuidInternal
|
||||
};
|
||||
|
||||
uint count = connectInterface.GetProductUserExternalAccountCount(ref getProductUserExternalAccountCountOptions);
|
||||
var accountIds = new AccountId[count];
|
||||
|
||||
foreach (int i in Enumerable.Range(0, (int)count))
|
||||
{
|
||||
var copyProductUserExternalAccountByIndexOptions = new Epic.OnlineServices.Connect.CopyProductUserExternalAccountByIndexOptions
|
||||
{
|
||||
TargetUserId = otherPuidInternal,
|
||||
ExternalAccountInfoIndex = (uint)i
|
||||
};
|
||||
|
||||
connectInterface.CopyProductUserExternalAccountByIndex(
|
||||
ref copyProductUserExternalAccountByIndexOptions,
|
||||
out var externalAccountInfoNullable);
|
||||
if (!externalAccountInfoNullable.TryGetValue(out var externalAccountInfo))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.InvalidUser);
|
||||
}
|
||||
|
||||
var accountIdOption =
|
||||
EosStringToAccountId(externalAccountInfo.AccountId, externalAccountInfo.AccountIdType);
|
||||
if (!accountIdOption.TryUnwrap(out var accountId))
|
||||
{
|
||||
return failure(EosInterface.IdQueries.GetSelfExternalIdError.ParseError);
|
||||
}
|
||||
|
||||
accountIds[i] = accountId;
|
||||
}
|
||||
|
||||
return success(accountIds.ToImmutableArray());
|
||||
}
|
||||
|
||||
internal static async Task<Result<EosInterface.ProductUserId, Epic.OnlineServices.Result>> GetPuidForExternalId(AccountId externalId)
|
||||
{
|
||||
var connectInterface = CorePrivate.ConnectInterface;
|
||||
if (connectInterface is null)
|
||||
{
|
||||
return Result.Failure(Epic.OnlineServices.Result.NotConfigured);
|
||||
}
|
||||
|
||||
var externalAccountType = externalId is EpicAccountId
|
||||
? Epic.OnlineServices.ExternalAccountType.Epic
|
||||
: Epic.OnlineServices.ExternalAccountType.Steam;
|
||||
string externalAccountEosRepresentation = externalId.EosStringRepresentation;
|
||||
|
||||
Result<EosInterface.ProductUserId, Epic.OnlineServices.Result> lastError
|
||||
= Result.Failure(Epic.OnlineServices.Result.UnexpectedError);
|
||||
foreach (var selfPuid in GetLoggedInPuids()
|
||||
.OrderByDescending(id => LoginPrivate.PuidToPrimaryExternalId[id].GetType() == externalId.GetType()))
|
||||
{
|
||||
var selfPuidInternal = Epic.OnlineServices.ProductUserId.FromString(selfPuid.Value);
|
||||
|
||||
// See https://dev.epicgames.com/docs/en-US/api-ref/functions/eos-connect-query-external-account-mappings
|
||||
// to learn why we need to call this function before we call GetExternalAccountMapping
|
||||
var queryExternalAccountMappingsOptions = new Epic.OnlineServices.Connect.QueryExternalAccountMappingsOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
AccountIdType = externalAccountType,
|
||||
ExternalAccountIds = new Epic.OnlineServices.Utf8String[]
|
||||
{
|
||||
externalAccountEosRepresentation
|
||||
}
|
||||
};
|
||||
|
||||
var queryExternalAccountMappingsWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.QueryExternalAccountMappingsCallbackInfo>();
|
||||
connectInterface.QueryExternalAccountMappings(options: ref queryExternalAccountMappingsOptions, clientData: null, completionDelegate: queryExternalAccountMappingsWaiter.OnCompletion);
|
||||
var resultOption = await queryExternalAccountMappingsWaiter.Task;
|
||||
if (!resultOption.TryUnwrap(out var result))
|
||||
{
|
||||
lastError = Result.Failure(Epic.OnlineServices.Result.TimedOut);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
lastError = Result.Failure(result.ResultCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
var getExternalAccountMappingsOptions = new Epic.OnlineServices.Connect.GetExternalAccountMappingsOptions
|
||||
{
|
||||
LocalUserId = selfPuidInternal,
|
||||
AccountIdType = externalAccountType,
|
||||
TargetExternalUserId = externalAccountEosRepresentation
|
||||
};
|
||||
var otherPuid = connectInterface.GetExternalAccountMapping(ref getExternalAccountMappingsOptions);
|
||||
if (otherPuid is null)
|
||||
{
|
||||
lastError = Result.Failure(Epic.OnlineServices.Result.NotFound);
|
||||
continue;
|
||||
}
|
||||
return Result.Success(new EosInterface.ProductUserId(otherPuid.ToString()));
|
||||
}
|
||||
|
||||
return lastError;
|
||||
}
|
||||
|
||||
public static Option<AccountId> EosStringToAccountId(
|
||||
string stringRepresentation,
|
||||
Epic.OnlineServices.ExternalAccountType accountType)
|
||||
=> accountType switch
|
||||
{
|
||||
Epic.OnlineServices.ExternalAccountType.Steam => SteamId.Parse(stringRepresentation).Select(id => (AccountId)id),
|
||||
Epic.OnlineServices.ExternalAccountType.Epic => EpicAccountId.Parse(stringRepresentation).Select(id => (AccountId)id),
|
||||
_ => Option.None
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override ImmutableArray<EosInterface.ProductUserId> GetLoggedInPuids()
|
||||
=> IdQueriesPrivate.GetLoggedInPuids();
|
||||
|
||||
public override ImmutableArray<EpicAccountId> GetLoggedInEpicIds()
|
||||
=> IdQueriesPrivate.GetLoggedInEpicIds();
|
||||
|
||||
public override Task<Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError>> GetSelfExternalAccountIds(EosInterface.ProductUserId puid)
|
||||
=> TaskScheduler.Schedule(() => IdQueriesPrivate.GetSelfExternalAccountIds(puid));
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Debugging;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static class LoginPrivate
|
||||
{
|
||||
private const string EosLoginSteamIdentity = "BarotraumaEosLogin";
|
||||
private static Option<Steamworks.AuthTicketForWebApi> steamworksAuthTicket;
|
||||
|
||||
private static Option<ulong> eosConnectExpirationNotifyId, eosConnectStatusChangedNotifyId;
|
||||
private static Option<ulong> egsAuthExpirationNotifyId;
|
||||
|
||||
internal static void Init()
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return; }
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return; }
|
||||
|
||||
ClearNotificationId(ref egsAuthExpirationNotifyId, egsAuthInterface.RemoveNotifyLoginStatusChanged);
|
||||
var authExpirationOptions = new Epic.OnlineServices.Auth.AddNotifyLoginStatusChangedOptions();
|
||||
ulong authExpirationNotifyId = egsAuthInterface.AddNotifyLoginStatusChanged(ref authExpirationOptions, null, OnEgsAuthStatusChanged);
|
||||
StoreNotificationId(out egsAuthExpirationNotifyId, authExpirationNotifyId);
|
||||
|
||||
ClearNotificationId(ref eosConnectExpirationNotifyId, connectInterface.RemoveNotifyAuthExpiration);
|
||||
var connectExpirationOptions = new Epic.OnlineServices.Connect.AddNotifyAuthExpirationOptions();
|
||||
ulong connectExpirationNotifyId = connectInterface.AddNotifyAuthExpiration(ref connectExpirationOptions, null, OnConnectExpiration);
|
||||
StoreNotificationId(out eosConnectExpirationNotifyId, connectExpirationNotifyId);
|
||||
|
||||
ClearNotificationId(ref eosConnectStatusChangedNotifyId, connectInterface.RemoveNotifyLoginStatusChanged);
|
||||
var addNotifyConnectStatusChangedOptions = new Epic.OnlineServices.Connect.AddNotifyLoginStatusChangedOptions();
|
||||
var connectChangedNotifyId = connectInterface.AddNotifyLoginStatusChanged(ref addNotifyConnectStatusChangedOptions, null, OnConnectStatusChanged);
|
||||
StoreNotificationId(out eosConnectStatusChangedNotifyId, connectChangedNotifyId);
|
||||
|
||||
static void ClearNotificationId(ref Option<ulong> field, Action<ulong> clearAction)
|
||||
{
|
||||
if (field.TryUnwrap(out var notificationId))
|
||||
{
|
||||
clearAction(notificationId);
|
||||
}
|
||||
field = Option.None;
|
||||
}
|
||||
|
||||
static void StoreNotificationId(out Option<ulong> field, ulong value)
|
||||
{
|
||||
bool isValid = value is not Epic.OnlineServices.Common.InvalidNotificationid;
|
||||
field = isValid
|
||||
? Option.Some(value)
|
||||
: Option.None;
|
||||
}
|
||||
}
|
||||
|
||||
internal static readonly ConcurrentDictionary<EosInterface.ProductUserId, AccountId> PuidToPrimaryExternalId = new();
|
||||
|
||||
private readonly record struct LoginParams(
|
||||
Epic.OnlineServices.Connect.Credentials Credentials,
|
||||
AccountId ExternalAccountId);
|
||||
|
||||
private static async Task<Result<LoginParams, EosInterface.Login.LoginError>> GenCredentialsSteam()
|
||||
{
|
||||
if (!Steamworks.SteamClient.IsValid || !Steamworks.SteamClient.IsLoggedOn) { return Result.Failure(EosInterface.Login.LoginError.SteamNotLoggedIn); }
|
||||
if (steamworksAuthTicket.TryUnwrap(out var oldTicket)) { oldTicket.Cancel(); }
|
||||
var newTicketNullable = await Steamworks.SteamUser.GetAuthTicketForWebApi(EosLoginSteamIdentity);
|
||||
if (newTicketNullable is not { Data: not null } ticket)
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.FailedToGetSteamSessionTicket);
|
||||
}
|
||||
return Result.Success(
|
||||
new LoginParams(
|
||||
Credentials: new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = ToolBoxCore.ByteArrayToHexString(ticket.Data),
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.SteamSessionTicket
|
||||
},
|
||||
ExternalAccountId: new SteamId(Steamworks.SteamClient.SteamId)));
|
||||
}
|
||||
|
||||
private static async Task<Result<Either<LoginParams, Epic.OnlineServices.ContinuanceToken>, EosInterface.Login.LoginError>> GenCredentialsEpic(
|
||||
Epic.OnlineServices.Auth.LoginCredentialType credentialsType,
|
||||
string? credentialsId,
|
||||
string? credentialsToken,
|
||||
Epic.OnlineServices.ExternalCredentialType credentialsExternalType,
|
||||
EosInterface.Login.LoginEpicFlags flags)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LoginError.EosNotInitialized); }
|
||||
|
||||
if (credentialsType is not (
|
||||
Epic.OnlineServices.Auth.LoginCredentialType.ExternalAuth
|
||||
or Epic.OnlineServices.Auth.LoginCredentialType.Developer
|
||||
or Epic.OnlineServices.Auth.LoginCredentialType.ExchangeCode))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.InvalidUser);
|
||||
}
|
||||
|
||||
var authLoginOptions = new Epic.OnlineServices.Auth.LoginOptions
|
||||
{
|
||||
Credentials = new Epic.OnlineServices.Auth.Credentials
|
||||
{
|
||||
Id = credentialsId,
|
||||
Token = credentialsToken,
|
||||
Type = credentialsType,
|
||||
SystemAuthCredentialsOptions = default,
|
||||
ExternalType = credentialsExternalType
|
||||
},
|
||||
ScopeFlags =
|
||||
Epic.OnlineServices.Auth.AuthScopeFlags.BasicProfile
|
||||
| Epic.OnlineServices.Auth.AuthScopeFlags.Presence
|
||||
| Epic.OnlineServices.Auth.AuthScopeFlags.FriendsList,
|
||||
LoginFlags = flags.HasFlag(EosInterface.Login.LoginEpicFlags.FailWithoutOpeningBrowser)
|
||||
? Epic.OnlineServices.Auth.LoginFlags.NoUserInterface
|
||||
: Epic.OnlineServices.Auth.LoginFlags.None
|
||||
};
|
||||
|
||||
var authLoginWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.LoginCallbackInfo>();
|
||||
egsAuthInterface.Login(options: ref authLoginOptions, clientData: null, completionDelegate: authLoginWaiter.OnCompletion);
|
||||
|
||||
// This can time out if authLoginOptions.ScopeFlags is set incorrectly,
|
||||
// because the docs lied and this callback isn't guaranteed to be called
|
||||
var authLoginCallbackInfoOption = await authLoginWaiter.Task;
|
||||
|
||||
if (!authLoginCallbackInfoOption.TryUnwrap(out var authLoginCallbackInfo))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.EgsLoginTimeout);
|
||||
}
|
||||
if (authLoginCallbackInfo is { ResultCode: Epic.OnlineServices.Result.InvalidUser, ContinuanceToken: { } continuanceToken })
|
||||
{
|
||||
return Result.Success((Either<LoginParams, Epic.OnlineServices.ContinuanceToken>)continuanceToken);
|
||||
}
|
||||
|
||||
if (authLoginCallbackInfo.ResultCode != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Failure(authLoginCallbackInfo.ResultCode switch {
|
||||
Epic.OnlineServices.Result.NotFound
|
||||
=> EosInterface.Login.LoginError.EgsAccountNotFound,
|
||||
Epic.OnlineServices.Result.AuthExchangeCodeNotFound
|
||||
=> EosInterface.Login.LoginError.AuthExchangeCodeNotFound,
|
||||
Epic.OnlineServices.Result.AuthUserInterfaceRequired
|
||||
=> EosInterface.Login.LoginError.AuthRequiresOpeningBrowser,
|
||||
Epic.OnlineServices.Result.AccessDenied
|
||||
=> EosInterface.Login.LoginError.EgsAccessDenied,
|
||||
_
|
||||
=> EosInterface.Login.LoginError.UnhandledFailureCondition
|
||||
});
|
||||
}
|
||||
if (!EpicAccountId.Parse(authLoginCallbackInfo.LocalUserId.ToString()).TryUnwrap(out var externalAccountId))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.FailedToParseEgsId);
|
||||
}
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions
|
||||
{
|
||||
AccountId = authLoginCallbackInfo.LocalUserId
|
||||
};
|
||||
|
||||
var tokenCopyResult = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var tokenNullable);
|
||||
if (tokenCopyResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
Result.Failure(EosInterface.Login.LoginError.FailedToGetEgsIdToken);
|
||||
}
|
||||
if (tokenNullable is not { } token) { return Result.Failure(EosInterface.Login.LoginError.FailedToGetEgsIdToken); }
|
||||
|
||||
return Result.Success(
|
||||
(Either<LoginParams, Epic.OnlineServices.ContinuanceToken>)new LoginParams(
|
||||
Credentials: new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = token.JsonWebToken,
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken
|
||||
},
|
||||
ExternalAccountId: externalAccountId));
|
||||
}
|
||||
|
||||
public static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginSteam()
|
||||
{
|
||||
var credentialsSteamResult = await GenCredentialsSteam();
|
||||
if (credentialsSteamResult.TryUnwrapFailure(out var error))
|
||||
{
|
||||
return Result.Failure(error);
|
||||
}
|
||||
if (!credentialsSteamResult.TryUnwrapSuccess(out var loginParams))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.InvalidUser);
|
||||
}
|
||||
|
||||
var result = await Login(loginParams);
|
||||
if (steamworksAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); }
|
||||
steamworksAuthTicket = Option.None;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<Result<OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags)
|
||||
{
|
||||
if (steamworksAuthTicket.TryUnwrap(out var oldTicket)) { oldTicket.Cancel(); }
|
||||
var newTicketNullable = await Steamworks.SteamUser.GetAuthTicketForWebApi(EosLoginSteamIdentity);
|
||||
if (newTicketNullable is not { Data: not null } ticket)
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.FailedToGetSteamSessionTicket);
|
||||
}
|
||||
var epicCredentialsOption = await GenCredentialsEpic(
|
||||
credentialsType: Epic.OnlineServices.Auth.LoginCredentialType.ExternalAuth,
|
||||
credentialsId: null,
|
||||
credentialsToken: ToolBoxCore.ByteArrayToHexString(ticket.Data),
|
||||
credentialsExternalType: Epic.OnlineServices.ExternalCredentialType.SteamSessionTicket,
|
||||
flags: flags);
|
||||
if (epicCredentialsOption.TryUnwrapFailure(out var epicCredentialsFail))
|
||||
{
|
||||
return Result.Failure(epicCredentialsFail);
|
||||
}
|
||||
if (!epicCredentialsOption.TryUnwrapSuccess(out var loginParamsOrContinuanceToken))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
|
||||
if (loginParamsOrContinuanceToken.TryGet(out Epic.OnlineServices.ContinuanceToken continuanceToken))
|
||||
{
|
||||
return Result.Success((OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>)
|
||||
new EosInterface.EgsAuthContinuanceToken(continuanceToken.InnerHandle, ExtractExpiryTimeFromContinuanceToken(continuanceToken, EosInterface.EgsAuthContinuanceToken.Duration)));
|
||||
}
|
||||
if (!loginParamsOrContinuanceToken.TryGet(out LoginParams loginParams))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnexpectedContinuanceToken);
|
||||
}
|
||||
|
||||
var loginResult = await Login(loginParams);
|
||||
if (loginResult.TryUnwrapSuccess(out var loginSuccess))
|
||||
{
|
||||
return loginSuccess.TryGet(out EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
? Result.Success((OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>)eosContinuanceToken)
|
||||
: loginSuccess.TryGet(out EosInterface.ProductUserId puid)
|
||||
? Result.Success((OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>)puid)
|
||||
: Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
return loginResult.TryUnwrapFailure(out var loginFailure)
|
||||
? Result.Failure(loginFailure)
|
||||
: Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
|
||||
public static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode)
|
||||
{
|
||||
var epicCredentialsOption = await GenCredentialsEpic(
|
||||
credentialsType: Epic.OnlineServices.Auth.LoginCredentialType.ExchangeCode,
|
||||
credentialsId: "",
|
||||
credentialsToken: exchangeCode,
|
||||
credentialsExternalType: Epic.OnlineServices.ExternalCredentialType.Epic,
|
||||
flags: EosInterface.Login.LoginEpicFlags.None);
|
||||
if (epicCredentialsOption.TryUnwrapFailure(out var epicCredentialsFail))
|
||||
{
|
||||
return Result.Failure(epicCredentialsFail);
|
||||
}
|
||||
if (!epicCredentialsOption.TryUnwrapSuccess(out var loginParamsOrContinuanceToken))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnhandledFailureCondition);
|
||||
}
|
||||
if (!loginParamsOrContinuanceToken.TryGet(out LoginParams loginParams))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LoginError.UnexpectedContinuanceToken);
|
||||
}
|
||||
|
||||
var result = await Login(loginParams);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken egsIdToken)
|
||||
{
|
||||
if (egsIdToken is not EgsIdTokenPrivate privateEgsIdToken) { return Result.Failure(EosInterface.Login.LoginError.InvalidUser); }
|
||||
var credentials = new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = privateEgsIdToken.InternalToken.JsonWebToken,
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken
|
||||
};
|
||||
|
||||
return await Login(new LoginParams(credentials, privateEgsIdToken.AccountId));
|
||||
}
|
||||
|
||||
private static DateTime ExtractExpiryTimeFromContinuanceToken(Epic.OnlineServices.ContinuanceToken continuanceToken, TimeSpan fallbackDuration)
|
||||
{
|
||||
// Not the exact expiry time, but it's a pretty close guess should we fail to decode the continuance token
|
||||
var expiryTime = DateTime.Now + fallbackDuration;
|
||||
|
||||
// This method exists to replace Epic.OnlineServices.ContinuanceToken.ToString because
|
||||
// the generated code is broken, and I don't want to modify it because we risk undoing
|
||||
// a fix when we update the SDK.
|
||||
static string continuanceTokenToString(Epic.OnlineServices.ContinuanceToken continuanceToken)
|
||||
{
|
||||
int inOutBufferLength = 1024;
|
||||
System.IntPtr outBufferAddress = Epic.OnlineServices.Helper.AddAllocation(inOutBufferLength);
|
||||
|
||||
var funcResult = Epic.OnlineServices.Bindings.EOS_ContinuanceToken_ToString(continuanceToken.InnerHandle, outBufferAddress, ref inOutBufferLength);
|
||||
if (funcResult == Epic.OnlineServices.Result.LimitExceeded)
|
||||
{
|
||||
// Buffer wasn't large enough to copy the string.
|
||||
// inOutBufferLength was updated by the last call to be the actual length required.
|
||||
// Generate a new buffer and try again.
|
||||
Epic.OnlineServices.Helper.Dispose(ref outBufferAddress);
|
||||
outBufferAddress = Epic.OnlineServices.Helper.AddAllocation(inOutBufferLength);
|
||||
funcResult = Epic.OnlineServices.Bindings.EOS_ContinuanceToken_ToString(continuanceToken.InnerHandle, outBufferAddress, ref inOutBufferLength);
|
||||
if (funcResult != Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
DebugConsoleCore.Log($"EOS_ContinuanceToken_ToString failed with result {funcResult}");
|
||||
}
|
||||
}
|
||||
|
||||
Epic.OnlineServices.Utf8String outBuffer = "EOS_ContinuanceToken_ToString failed";
|
||||
if (funcResult == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
Epic.OnlineServices.Helper.Get(outBufferAddress, out outBuffer);
|
||||
}
|
||||
Epic.OnlineServices.Helper.Dispose(ref outBufferAddress);
|
||||
|
||||
return outBuffer;
|
||||
}
|
||||
|
||||
var ctDecode = JsonWebToken.Parse(continuanceTokenToString(continuanceToken));
|
||||
if (ctDecode.TryUnwrap(out var jwt))
|
||||
{
|
||||
string decodedPayload = jwt.PayloadDecoded;
|
||||
try
|
||||
{
|
||||
// Ugly regex hack to get expiry time. The right thing to do would be to parse the payload as JSON,
|
||||
// but I don't really care because we're extracting one field out of this whole thing.
|
||||
string expiryTimeUnix = Regex.Match(decodedPayload, @"""exp""\s*:\s*([0-9]+)").Groups[1].Value;
|
||||
expiryTime = UnixTime.ParseUtc(expiryTimeUnix).Fallback(UnixTime.UtcEpoch).ToLocalTime();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// could not extract expiry time, oh well!
|
||||
}
|
||||
}
|
||||
|
||||
return expiryTime;
|
||||
}
|
||||
|
||||
private static async Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> Login(LoginParams loginParams)
|
||||
{
|
||||
static Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError> success(EosInterface.ProductUserId id)
|
||||
=> Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>.Success(id);
|
||||
static Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError> continuance(EosInterface.EosConnectContinuanceToken token)
|
||||
=> Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>.Success(token);
|
||||
static Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError> failure(EosInterface.Login.LoginError error)
|
||||
=> Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>.Failure(error);
|
||||
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return failure(EosInterface.Login.LoginError.EosNotInitialized); }
|
||||
|
||||
var loginOptions = new Epic.OnlineServices.Connect.LoginOptions
|
||||
{
|
||||
Credentials = loginParams.Credentials,
|
||||
UserLoginInfo = null
|
||||
};
|
||||
AccountId primaryExternalId = loginParams.ExternalAccountId;
|
||||
|
||||
var loginWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.LoginCallbackInfo>();
|
||||
connectInterface.Login(options: ref loginOptions, clientData: null, completionDelegate: loginWaiter.OnCompletion);
|
||||
var callbackResultOption = await loginWaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return failure(EosInterface.Login.LoginError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
var retVal = new EosInterface.ProductUserId(callbackResult.LocalUserId.ToString());
|
||||
PuidToPrimaryExternalId[retVal] = primaryExternalId;
|
||||
return success(retVal);
|
||||
}
|
||||
|
||||
if (callbackResult is { ResultCode: Epic.OnlineServices.Result.InvalidUser, ContinuanceToken: { } continuanceToken })
|
||||
{
|
||||
var expiryTime = ExtractExpiryTimeFromContinuanceToken(continuanceToken, EosInterface.EosConnectContinuanceToken.Duration);
|
||||
|
||||
return continuance(new EosInterface.EosConnectContinuanceToken(callbackResult.ContinuanceToken.InnerHandle, primaryExternalId, expiryTime));
|
||||
}
|
||||
|
||||
return callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidUser
|
||||
=> failure(EosInterface.Login.LoginError.InvalidUser),
|
||||
Epic.OnlineServices.Result.AccessDenied
|
||||
=> failure(EosInterface.Login.LoginError.EosAccessDenied),
|
||||
var unhandled
|
||||
=> failure(unhandled.FailAndLogUnhandledError(EosInterface.Login.LoginError.UnhandledFailureCondition))
|
||||
};
|
||||
}
|
||||
|
||||
private static void OnEgsAuthStatusChanged(ref Epic.OnlineServices.Auth.LoginStatusChangedCallbackInfo info)
|
||||
{
|
||||
var eaidOption = EpicAccountId.Parse(info.LocalUserId.ToString());
|
||||
if (!eaidOption.TryUnwrap(out var eaid)) { return; }
|
||||
|
||||
if (info.CurrentStatus == Epic.OnlineServices.LoginStatus.NotLoggedIn)
|
||||
{
|
||||
TaskPool.Add(
|
||||
"UnlogPuidLinkedToEaid",
|
||||
IdQueriesPrivate.GetPuidForExternalId(eaid),
|
||||
t =>
|
||||
{
|
||||
if (!t.TryGetResult(out Result<EosInterface.ProductUserId, Epic.OnlineServices.Result>? result)) { return; }
|
||||
if (!result.TryUnwrapSuccess(out var puid)) { return; }
|
||||
|
||||
MarkAsInaccessible(puid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnConnectExpiration(ref Epic.OnlineServices.Connect.AuthExpirationCallbackInfo info)
|
||||
{
|
||||
var puid = new EosInterface.ProductUserId(info.LocalUserId.ToString());
|
||||
DebugConsoleCore.Log($"OnAuthExpirationNotification {puid}");
|
||||
if (!PuidToPrimaryExternalId.TryGetValue(puid, out var externalId)) { return; }
|
||||
|
||||
switch (externalId)
|
||||
{
|
||||
case SteamId:
|
||||
{
|
||||
static async Task RelogSteam()
|
||||
{
|
||||
var steamCredentialsResult = await GenCredentialsSteam();
|
||||
if (!steamCredentialsResult.TryUnwrapSuccess(out var loginParams)) { return; }
|
||||
await Relog(loginParams);
|
||||
}
|
||||
|
||||
TaskPool.Add(
|
||||
"EosReLoginSteam",
|
||||
RelogSteam(),
|
||||
TaskPool.IgnoredCallback);
|
||||
break;
|
||||
}
|
||||
case EpicAccountId epicAccountId:
|
||||
{
|
||||
if (CopyEpicIdToken(epicAccountId).TryUnwrap(out var token))
|
||||
{
|
||||
var epicLoginCredentials = new Epic.OnlineServices.Connect.Credentials
|
||||
{
|
||||
Token = token.JsonWebToken,
|
||||
Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken
|
||||
};
|
||||
var reLogParams = new LoginParams(Credentials: epicLoginCredentials, ExternalAccountId: externalId);
|
||||
TaskPool.Add("OnAuthExpirationNotification", Relog(reLogParams), onCompletion: TaskPool.IgnoredCallback);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task Relog(LoginParams loginParams)
|
||||
{
|
||||
var loginOptions = new Epic.OnlineServices.Connect.LoginOptions
|
||||
{
|
||||
Credentials = loginParams.Credentials,
|
||||
UserLoginInfo = null
|
||||
};
|
||||
|
||||
var connectLoginWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.LoginCallbackInfo>();
|
||||
CorePrivate.ConnectInterface?.Login(options: ref loginOptions, clientData: null, completionDelegate: connectLoginWaiter.OnCompletion);
|
||||
var resultOption = await connectLoginWaiter.Task;
|
||||
if (resultOption.TryUnwrap(out var result))
|
||||
{
|
||||
string s = $"EOS relog result: {result.ResultCode}";
|
||||
if (result.LocalUserId != null)
|
||||
{
|
||||
s += " : " + result.LocalUserId;
|
||||
}
|
||||
|
||||
if (result.ContinuanceToken != null)
|
||||
{
|
||||
s += " ; " + result.ContinuanceToken;
|
||||
}
|
||||
DebugConsoleCore.Log(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsoleCore.Log("EOS relog timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnConnectStatusChanged(ref Epic.OnlineServices.Connect.LoginStatusChangedCallbackInfo info)
|
||||
{
|
||||
var puid = new EosInterface.ProductUserId(info.LocalUserId.ToString());
|
||||
DebugConsoleCore.Log($"OnLoginStatusChangedNotification {puid} {info.CurrentStatus}");
|
||||
if (info.CurrentStatus == Epic.OnlineServices.LoginStatus.NotLoggedIn)
|
||||
{
|
||||
PuidToPrimaryExternalId.TryRemove(puid, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Result<EpicAccountId, EosInterface.Login.LinkExternalAccountToEpicAccountError>> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.EosNotInitialized); }
|
||||
|
||||
var linkOptions = new Epic.OnlineServices.Auth.LinkAccountOptions
|
||||
{
|
||||
LinkAccountFlags = Epic.OnlineServices.Auth.LinkAccountFlags.NoFlags,
|
||||
ContinuanceToken = new Epic.OnlineServices.ContinuanceToken(continuanceToken.Spend()),
|
||||
LocalUserId = null
|
||||
};
|
||||
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.LinkAccountCallbackInfo>(timeout: TimeSpan.FromMinutes(5));
|
||||
egsAuthInterface.LinkAccount(options: ref linkOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var resultOption = await callbackWaiter.Task;
|
||||
|
||||
if (!resultOption.TryUnwrap(out var result))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.TimedOut);
|
||||
}
|
||||
|
||||
if (result.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
if (!EpicAccountId.Parse(result.SelectedAccountId.ToString()).TryUnwrap(out var epicAccountId))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.FailedToParseEgsAccountId);
|
||||
}
|
||||
return Result.Success(epicAccountId);
|
||||
}
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountToEpicAccountError.UnhandledErrorCondition);
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Login.LogoutEpicAccountError>> LogoutEpicAccount(EpicAccountId egsId)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Result.Failure(EosInterface.Login.LogoutEpicAccountError.EosNotInitialized); }
|
||||
|
||||
var logoutOptions = new Epic.OnlineServices.Auth.LogoutOptions
|
||||
{
|
||||
LocalUserId = Epic.OnlineServices.EpicAccountId.FromString(egsId.EosStringRepresentation)
|
||||
};
|
||||
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Auth.LogoutCallbackInfo>();
|
||||
egsAuthInterface.Logout(options: ref logoutOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var logoutResultOption = await callbackWaiter.Task;
|
||||
if (!logoutResultOption.TryUnwrap(out var logoutResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LogoutEpicAccountError.TimedOut);
|
||||
}
|
||||
if (logoutResult.ResultCode == Epic.OnlineServices.Result.Success) { return Result.Success(Unit.Value); }
|
||||
|
||||
return Result.Failure(logoutResult.ResultCode switch
|
||||
{
|
||||
_
|
||||
=> EosInterface.Login.LogoutEpicAccountError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
|
||||
public static void MarkAsInaccessible(EosInterface.ProductUserId puid)
|
||||
{
|
||||
PuidToPrimaryExternalId.TryRemove(puid, out _);
|
||||
}
|
||||
|
||||
private static Option<Epic.OnlineServices.Auth.IdToken> CopyEpicIdToken(EpicAccountId epicAccountId)
|
||||
{
|
||||
if (CorePrivate.EgsAuthInterface is not { } egsAuthInterface) { return Option.None; }
|
||||
|
||||
var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions
|
||||
{
|
||||
AccountId = Epic.OnlineServices.EpicAccountId.FromString(epicAccountId.EosStringRepresentation)
|
||||
};
|
||||
var result = egsAuthInterface.CopyIdToken(ref copyIdTokenOptions, out var tokenNullable);
|
||||
|
||||
if (result is Epic.OnlineServices.Result.Success && tokenNullable is { } token)
|
||||
{
|
||||
return Option.Some(token);
|
||||
}
|
||||
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
public static async Task<Result<EosInterface.ProductUserId, EosInterface.Login.CreateProductAccountError>> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.CreateProductAccountError.EosNotInitialized); }
|
||||
if (eosContinuanceToken is not { IsValid: true }) { return Result.Failure(EosInterface.Login.CreateProductAccountError.InvalidContinuanceToken); }
|
||||
|
||||
var internalContinuanceToken = new Epic.OnlineServices.ContinuanceToken(eosContinuanceToken.Spend());
|
||||
var options = new Epic.OnlineServices.Connect.CreateUserOptions
|
||||
{
|
||||
ContinuanceToken = internalContinuanceToken
|
||||
};
|
||||
|
||||
var createUserWaiter = new CallbackWaiter<Epic.OnlineServices.Connect.CreateUserCallbackInfo>();
|
||||
connectInterface.CreateUser(options: ref options, clientData: null, completionDelegate: createUserWaiter.OnCompletion);
|
||||
var callbackResultOption = await createUserWaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.CreateProductAccountError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
var retVal = new EosInterface.ProductUserId(callbackResult.LocalUserId.ToString());
|
||||
PuidToPrimaryExternalId[retVal] = eosContinuanceToken.ExternalAccountId;
|
||||
return Result.Success(retVal);
|
||||
}
|
||||
|
||||
return Result.Failure(EosInterface.Login.CreateProductAccountError.UnhandledErrorCondition);
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Login.LinkExternalAccountError>> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.LinkExternalAccountError.EosNotInitialized); }
|
||||
if (eosContinuanceToken is not { IsValid: true }) { return Result.Failure(EosInterface.Login.LinkExternalAccountError.InvalidContinuanceToken); }
|
||||
|
||||
var internalContinuanceToken = new Epic.OnlineServices.ContinuanceToken(eosContinuanceToken.Spend());
|
||||
var internalPuid = Epic.OnlineServices.ProductUserId.FromString(puid.Value);
|
||||
var options = new Epic.OnlineServices.Connect.LinkAccountOptions
|
||||
{
|
||||
LocalUserId = internalPuid,
|
||||
ContinuanceToken = internalContinuanceToken
|
||||
};
|
||||
|
||||
var linkAccountAwaiter = new CallbackWaiter<Epic.OnlineServices.Connect.LinkAccountCallbackInfo>();
|
||||
connectInterface.LinkAccount(options: ref options, clientData: null, completionDelegate: linkAccountAwaiter.OnCompletion);
|
||||
var callbackResultOption = await linkAccountAwaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.LinkExternalAccountError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
return Result.Failure(callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.ConnectLinkAccountFailed
|
||||
=> EosInterface.Login.LinkExternalAccountError.CannotLink,
|
||||
_
|
||||
=> EosInterface.Login.LinkExternalAccountError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<Result<Unit, EosInterface.Login.UnlinkExternalAccountError>> UnlinkExternalAccount(EosInterface.ProductUserId puid)
|
||||
{
|
||||
if (CorePrivate.ConnectInterface is not { } connectInterface) { return Result.Failure(EosInterface.Login.UnlinkExternalAccountError.EosNotInitialized); }
|
||||
|
||||
var internalPuid = Epic.OnlineServices.ProductUserId.FromString(puid.Value);
|
||||
var options = new Epic.OnlineServices.Connect.UnlinkAccountOptions
|
||||
{
|
||||
LocalUserId = internalPuid
|
||||
};
|
||||
|
||||
var unlinkAccountAwaiter = new CallbackWaiter<Epic.OnlineServices.Connect.UnlinkAccountCallbackInfo>();
|
||||
connectInterface.UnlinkAccount(options: ref options, clientData: null, completionDelegate: unlinkAccountAwaiter.OnCompletion);
|
||||
var callbackResultOption = await unlinkAccountAwaiter.Task;
|
||||
if (!callbackResultOption.TryUnwrap(out var callbackResult))
|
||||
{
|
||||
return Result.Failure(EosInterface.Login.UnlinkExternalAccountError.Timeout);
|
||||
}
|
||||
|
||||
if (callbackResult.ResultCode == Epic.OnlineServices.Result.Success)
|
||||
{
|
||||
PuidToPrimaryExternalId.TryRemove(puid, out _);
|
||||
return Result.Success(Unit.Value);
|
||||
}
|
||||
|
||||
return Result.Failure(callbackResult.ResultCode switch
|
||||
{
|
||||
Epic.OnlineServices.Result.InvalidUser
|
||||
=> EosInterface.Login.UnlinkExternalAccountError.InvalidUser,
|
||||
_
|
||||
=> EosInterface.Login.UnlinkExternalAccountError.UnhandledErrorCondition
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Result<EosInterface.ProductUserId, EosInterface.Login.CreateProductAccountError>> CreateProductAccount(EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.CreateProductAccount(eosContinuanceToken));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Login.LinkExternalAccountError>> LinkExternalAccount(EosInterface.ProductUserId puid, EosInterface.EosConnectContinuanceToken eosContinuanceToken)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccount(puid, eosContinuanceToken));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Login.UnlinkExternalAccountError>> UnlinkExternalAccount(EosInterface.ProductUserId puid)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.UnlinkExternalAccount(puid));
|
||||
|
||||
public override Task<Result<OneOf<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken, EosInterface.EgsAuthContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicWithLinkedSteamAccount(EosInterface.Login.LoginEpicFlags flags)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LoginEpicWithLinkedSteamAccount(flags));
|
||||
|
||||
public override Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicExchangeCode(string exchangeCode)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LoginEpicExchangeCode(exchangeCode));
|
||||
|
||||
public override Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginEpicIdToken(EosInterface.EgsIdToken token)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LoginEpicIdToken(token));
|
||||
|
||||
public override Task<Result<Either<EosInterface.ProductUserId, EosInterface.EosConnectContinuanceToken>, EosInterface.Login.LoginError>> LoginSteam()
|
||||
=> TaskScheduler.Schedule(LoginPrivate.LoginSteam);
|
||||
|
||||
public override Task<Result<EpicAccountId, EosInterface.Login.LinkExternalAccountToEpicAccountError>> LinkExternalAccountToEpicAccount(EosInterface.EgsAuthContinuanceToken continuanceToken)
|
||||
=> TaskScheduler.Schedule(() => LoginPrivate.LinkExternalAccountToEpicAccount(continuanceToken));
|
||||
|
||||
public override Task<Result<Unit, EosInterface.Login.LogoutEpicAccountError>> LogoutEpicAccount(EpicAccountId egsId)
|
||||
=> LoginPrivate.LogoutEpicAccount(egsId);
|
||||
|
||||
public override void MarkAsInaccessible(EosInterface.ProductUserId puid)
|
||||
=> LoginPrivate.MarkAsInaccessible(puid);
|
||||
|
||||
public override void TestEosSessionTimeoutRecovery(EosInterface.ProductUserId puid)
|
||||
{
|
||||
var info = new Epic.OnlineServices.Connect.AuthExpirationCallbackInfo
|
||||
{
|
||||
ClientData = null,
|
||||
LocalUserId = Epic.OnlineServices.ProductUserId.FromString(puid.Value)
|
||||
};
|
||||
LoginPrivate.OnConnectExpiration(ref info);
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma;
|
||||
|
||||
namespace EosInterfacePrivate;
|
||||
|
||||
static partial class OwnershipPrivate
|
||||
{
|
||||
internal static async Task<Option<EosInterface.Ownership.Token>> GetGameOwnershipToken(EpicAccountId selfEpicAccountId)
|
||||
{
|
||||
if (CorePrivate.EcomInterface is not { } ecomInterface) { return Option.None; }
|
||||
|
||||
var epicAccountIdInternal =
|
||||
Epic.OnlineServices.EpicAccountId.FromString(selfEpicAccountId.EosStringRepresentation);
|
||||
|
||||
var queryOwnershipTokenOptions = new Epic.OnlineServices.Ecom.QueryOwnershipTokenOptions
|
||||
{
|
||||
LocalUserId = epicAccountIdInternal,
|
||||
CatalogItemIds = new Epic.OnlineServices.Utf8String[]
|
||||
{
|
||||
AudienceItemId,
|
||||
//"Completely arbitrary string!"
|
||||
|
||||
// IDEA:
|
||||
// As of 2023-06-21, QueryOwnershipToken will succeed even if given obviously fake catalog item IDs.
|
||||
// This could be useful to us! We could use this to add an audience parameter to this method and fix
|
||||
// the impersonation exploit without requiring our own persistent service.
|
||||
// We should ask Epic about this before actually trying it, this is certainly a hack and might get patched.
|
||||
}
|
||||
};
|
||||
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Ecom.QueryOwnershipTokenCallbackInfo>();
|
||||
ecomInterface.QueryOwnershipToken(options: ref queryOwnershipTokenOptions, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
|
||||
var queryOwnershipTokenResultOption = await callbackWaiter.Task;
|
||||
if (!queryOwnershipTokenResultOption.TryUnwrap(out var queryOwnershipTokenResult)) { return Option.None; }
|
||||
if (queryOwnershipTokenResult.ResultCode != Epic.OnlineServices.Result.Success) { return Option.None; }
|
||||
|
||||
var jwtOption = JsonWebToken.Parse(queryOwnershipTokenResult.OwnershipToken);
|
||||
return jwtOption.Select(jwt => new EosInterface.Ownership.Token(jwt));
|
||||
}
|
||||
|
||||
internal static async Task<Option<EpicAccountId>> VerifyGameOwnershipToken(EosInterface.Ownership.Token token)
|
||||
{
|
||||
JsonWebToken jwt = token.Jwt;
|
||||
|
||||
// Decode header
|
||||
string kidProperty;
|
||||
string algProperty;
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonDocument.Parse(jwt.HeaderDecoded);
|
||||
kidProperty = jsonDoc.RootElement.GetProperty("kid").GetString() ?? "";
|
||||
algProperty = jsonDoc.RootElement.GetProperty("alg").GetString() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Header JSON decode failed, can't verify token
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
// Basic header sanity checks
|
||||
if (algProperty != "RS512") { return Option.None; }
|
||||
if (!kidProperty.IsBase64Url()) { return Option.None; }
|
||||
|
||||
// Decode payload
|
||||
string epicAccountIdStr;
|
||||
string catalogItemId;
|
||||
Option<DateTime> expirationOption;
|
||||
bool owned;
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonDocument.Parse(jwt.PayloadDecoded);
|
||||
epicAccountIdStr = jsonDoc.RootElement.GetProperty("sub").GetString() ?? "";
|
||||
var entProperty = jsonDoc.RootElement.GetProperty("ent").EnumerateArray().First();
|
||||
catalogItemId = entProperty.GetProperty("catalogItemId").GetString() ?? "";
|
||||
expirationOption = UnixTime.ParseUtc(jsonDoc.RootElement.GetProperty("exp").GetUInt64().ToString());
|
||||
owned = entProperty.GetProperty("owned").GetBoolean();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Payload JSON decode failed, can't verify token
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
// Check that the payload is actually what we want
|
||||
if (catalogItemId != AudienceItemId) { return Option.None; }
|
||||
if (!owned) { return Option.None; }
|
||||
if (!expirationOption.TryUnwrap(out var expiration)) { return Option.None; }
|
||||
if (DateTime.UtcNow >= expiration) { return Option.None; }
|
||||
|
||||
// Get the public key required to verify this token
|
||||
string modulus;
|
||||
string exponent;
|
||||
try
|
||||
{
|
||||
string url =
|
||||
"https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com/ecommerceintegration/api/public/publickeys/"
|
||||
+ kidProperty;
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.SendAsync(new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
new Uri(url)));
|
||||
if (!response.IsSuccessStatusCode) { return Option.None; }
|
||||
var responseStr = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var responseJsonDoc = JsonDocument.Parse(responseStr);
|
||||
if (kidProperty != responseJsonDoc.RootElement.GetProperty("kid").GetString()) { return Option.None; }
|
||||
|
||||
modulus = responseJsonDoc.RootElement.GetProperty("n").GetString() ?? "";
|
||||
exponent = responseJsonDoc.RootElement.GetProperty("e").GetString() ?? "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failed to query EG Ecom web API, can't verify token
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
// Prepare RSA-SHA512 and verify token
|
||||
var modulusBytesOption = Base64Url.DecodeBytes(modulus);
|
||||
if (!modulusBytesOption.TryUnwrap(out var modulusBytes)) { return Option.None; }
|
||||
var exponentBytesOption = Base64Url.DecodeBytes(exponent);
|
||||
if (!exponentBytesOption.TryUnwrap(out var exponentBytes)) { return Option.None; }
|
||||
|
||||
var signatureBytesOption = Base64Url.DecodeBytes(jwt.Signature);
|
||||
if (!signatureBytesOption.TryUnwrap(out var signatureBytes)) { return Option.None; }
|
||||
|
||||
using var rsa = RSA.Create();
|
||||
using var sha = SHA512.Create();
|
||||
rsa.ImportParameters(new RSAParameters
|
||||
{
|
||||
Exponent = exponentBytes.ToArray(),
|
||||
Modulus = modulusBytes.ToArray(),
|
||||
});
|
||||
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(jwt.Header + "." + jwt.Payload));
|
||||
var deformatter = new RSAPKCS1SignatureDeformatter(rsa);
|
||||
deformatter.SetHashAlgorithm("SHA512");
|
||||
bool verified = deformatter.VerifySignature(hash, signatureBytes.ToArray());
|
||||
if (!verified) { return Option.None; }
|
||||
|
||||
return EpicAccountId.Parse(epicAccountIdStr);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
|
||||
{
|
||||
public override Task<Option<EosInterface.Ownership.Token>> GetGameOwnershipToken(EpicAccountId selfEpicAccountId)
|
||||
=> TaskScheduler.Schedule(() => OwnershipPrivate.GetGameOwnershipToken(selfEpicAccountId));
|
||||
|
||||
public override Task<Option<EpicAccountId>> VerifyGameOwnershipToken(EosInterface.Ownership.Token token)
|
||||
=> TaskScheduler.Schedule(() => OwnershipPrivate.VerifyGameOwnershipToken(token));
|
||||
}
|
||||
Reference in New Issue
Block a user