Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 124 additions & 30 deletions com.playeveryware.eos/Runtime/Core/EOSManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ namespace PlayEveryWare.EpicOnlineServices
using Extensions;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using UnityEngine.Assertions;
using AddNotifyLoginStatusChangedOptions = Epic.OnlineServices.Auth.AddNotifyLoginStatusChangedOptions;
using Credentials = Epic.OnlineServices.Auth.Credentials;
Expand Down Expand Up @@ -197,6 +198,10 @@ public partial class EOSSingleton
static private NotifyEventHandle s_notifyConnectLoginStatusChangedCallbackHandle;
static private NotifyEventHandle s_notifyConnectAuthExpirationCallbackHandle;

// --- Connect reauth hardening (prevents PUID churn / P2P route resets) ---
static private int s_connectReauthInProgress = 0;// Last Connect credential type used. Used to decide whether we can auto-refresh on auth expiration.
static private Epic.OnlineServices.ExternalCredentialType s_lastConnectCredentialType;

// Setting it twice will cause an exception
static bool hasSetLoggingCallback;

Expand Down Expand Up @@ -256,9 +261,14 @@ private string PUIDToString(ProductUserId puid)
/// <param name="localProductUserId"></param>
protected void SetLocalProductUserId(ProductUserId localProductUserId)
{
Log("Changing PUID: " + PUIDToString(s_localProductUserId) + " => " +
PUIDToString(localProductUserId));
var previous = s_localProductUserId;
s_localProductUserId = localProductUserId;

// Do not log user identifiers (PUID/EAS ids).
if (previous != null && localProductUserId != null && !previous.Equals(localProductUserId))
{
Log("[EOS] Local ProductUserId changed during runtime. This likely indicates account switch or login with different credentials. Game should tear down/rebuild P2P as needed.", LogType.Error);
}
}

//-------------------------------------------------------------------------
Expand Down Expand Up @@ -1076,7 +1086,7 @@ public async void StartConnectLoginWithEpicAccount(EpicAccountId epicAccountId,

if (result != Result.Success || !idToken.HasValue)
{
Debug.LogError($"{nameof(EOSManager)} {nameof(StartConnectLoginWithEpicAccount)}: CopyIdToken failed with result: {result}");
Log($"{nameof(EOSManager)} {nameof(StartConnectLoginWithEpicAccount)}: CopyIdToken failed with result: {result}", LogType.Error);
var dummy = new Epic.OnlineServices.Connect.LoginCallbackInfo
{
ResultCode = Result.InvalidAuth
Expand Down Expand Up @@ -1163,26 +1173,38 @@ public void StartConnectLoginWithOptions(ExternalCredentialType externalCredenti
public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions,
OnConnectLoginCallback onloginCallback)
{
CacheLastConnectCredentialType(connectLoginOptions);
// Do not log user identifiers (PUID/EAS ids).
Log($"[EOS][ConnectLogin] start cred={s_lastConnectCredentialType}");

var connectInterface = GetEOSPlatformInterface().GetConnectInterface();
connectInterface.Login(ref connectLoginOptions, null,
(ref Epic.OnlineServices.Connect.LoginCallbackInfo connectLoginData) =>
{
if (connectLoginData.ResultCode != Result.Success)
try
{
Log($"Connect login was not successful. ResultCode: {connectLoginData.ResultCode}", LogType.Error);
}
if (connectLoginData.ResultCode != Result.Success)
{
Log($"Connect login was not successful. ResultCode: {connectLoginData.ResultCode}", LogType.Error);
return;
}

if (connectLoginData.LocalUserId != null)
{
SetLocalProductUserId(connectLoginData.LocalUserId);
if (connectLoginData.LocalUserId == null)
{
Log("Connect login succeeded but LocalUserId is null (unexpected).", LogType.Error);
return;
}

var newPuid = connectLoginData.LocalUserId;
SetLocalProductUserId(newPuid);
ConfigureConnectStatusCallback();
ConfigureConnectExpirationCallback(connectLoginOptions);
OnConnectLogin?.Invoke(connectLoginData);
ConfigureConnectExpirationCallback();
}

if (onloginCallback != null)
finally
{
onloginCallback(connectLoginData);
// Always notify listeners/caller exactly once, regardless of early returns above.
OnConnectLogin?.Invoke(connectLoginData);
onloginCallback?.Invoke(connectLoginData);
}
});
}
Expand Down Expand Up @@ -1264,7 +1286,7 @@ public void StartPersistentLogin(OnAuthLoginCallback onLoginCallback)
public void StartLoginWithLoginTypeAndToken(LoginCredentialType loginType, string id, string token,
OnAuthLoginCallback onLoginCallback)
{
if(loginType == LoginCredentialType.ExchangeCode && string.IsNullOrEmpty(token))
if (loginType == LoginCredentialType.ExchangeCode && string.IsNullOrEmpty(token))
{
Debug.LogError($"{nameof(EOSManager)} {nameof(StartLoginWithLoginTypeAndToken)}: ExchangeCode login attempted with empty token. Abort login.");
onLoginCallback?.Invoke(new LoginCallbackInfo
Expand Down Expand Up @@ -1345,25 +1367,26 @@ private void ConfigureConnectStatusCallback()
}

//-------------------------------------------------------------------------
private void ConfigureConnectExpirationCallback(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions)
private void ConfigureConnectExpirationCallback()
{
if (s_notifyConnectAuthExpirationCallbackHandle == null)
{
var EOSConnectInterface = GetEOSConnectInterface();
var addNotifyAuthExpirationOptions = new AddNotifyAuthExpirationOptions();
ulong callbackHandle = EOSConnectInterface.AddNotifyAuthExpiration(
ref addNotifyAuthExpirationOptions, null, (ref AuthExpirationCallbackInfo callbackInfo) =>
{
StartConnectLoginWithOptions(connectLoginOptions, null);
});
if (s_notifyConnectAuthExpirationCallbackHandle != null)
return;

s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle =>
{
GetEOSConnectInterface()?.RemoveNotifyAuthExpiration(handle);
});
}
var eosConnectInterface = GetEOSConnectInterface();
var addNotifyAuthExpirationOptions = new AddNotifyAuthExpirationOptions();

ulong callbackHandle = eosConnectInterface.AddNotifyAuthExpiration(
ref addNotifyAuthExpirationOptions, null,
OnConnectAuthExpiration);
Log("[EOS][AuthExp] registered");
s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle =>
{
GetEOSConnectInterface()?.RemoveNotifyAuthExpiration(handle);
});
}



Comment on lines +1388 to +1389
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

//-------------------------------------------------------------------------
/// <summary>
/// Start an EOS Auth Login with the passed in LoginOptions. Call this instead of the method on EOSAuthInterface to ensure that
Expand Down Expand Up @@ -1792,6 +1815,77 @@ private static void UpdateNetworkStatus()

platformSpecifics?.UpdateNetworkStatus();
}

private void CacheLastConnectCredentialType(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions)
{
// Credentials is nullable: must validate HasValue.
if (!connectLoginOptions.Credentials.HasValue)
return;

s_lastConnectCredentialType = connectLoginOptions.Credentials.Value.Type;
}

/// <summary>
/// Handles Connect auth-expiration notifications.
///
/// Policy:
/// - If the last Connect credential type was EpicIdToken, the plugin can refresh by re-running Connect.Login
/// with a fresh Epic ID token (obtained via Auth.CopyIdToken) and restore Connect state.
/// - For other external credential types, the plugin cannot refresh tokens automatically; the game must
/// re-fetch the external token from the platform and call StartConnectLoginWithOptions again.
/// </summary>
private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo)
{
// Do not log user identifiers (PUID/EAS ids).
Log($"[EOS][AuthExp] received. CredentialType={s_lastConnectCredentialType}", LogType.Warning);

// Prevent concurrent re-auth attempts from repeated expiration notifications.
if (Interlocked.Exchange(ref s_connectReauthInProgress, 1) == 1)
{
Log($"{nameof(EOSManager)} Connect auth expiration ignored (reauth already in progress).", LogType.Warning);
return;
}

try
{
if (s_lastConnectCredentialType == Epic.OnlineServices.ExternalCredentialType.EpicIdToken)
{
RefreshConnectLoginWithFreshEpicIdToken();
}
else
{
Debug.LogWarning(
$"{nameof(EOSManager)} Connect auth expired but last credential type was {s_lastConnectCredentialType}. " +
"Plugin cannot refresh that token automatically. Game should re-fetch external token and call StartConnectLoginWithOptions again.");
}
}
finally
{
Interlocked.Exchange(ref s_connectReauthInProgress, 0);
}
}


private void RefreshConnectLoginWithFreshEpicIdToken()
{
// NOTE: EOS callbacks are expected to run on the same thread that drives PlatformInterface.Tick().
// In typical Unity integrations that's the main thread.
try
{
var localEpicUserId = GetLocalUserId();
if (localEpicUserId == null)
{
Log($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Local EpicAccountId is null.", LogType.Warning);
return;
}

StartConnectLoginWithEpicAccount(localEpicUserId, null);
}
catch(System.Exception ex)
{
Log($"{nameof(EOSManager)} {nameof(RefreshConnectLoginWithFreshEpicIdToken)} failed:{ex.GetType().Name}: {ex.Message}",LogType.Error);
}
}
}
#endif

Expand Down