diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index fa61b2fa8..dd7736f87 100644 --- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs +++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs @@ -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; @@ -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; @@ -256,9 +261,14 @@ private string PUIDToString(ProductUserId puid) /// 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); + } } //------------------------------------------------------------------------- @@ -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 @@ -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); } }); } @@ -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 @@ -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); + }); } + + //------------------------------------------------------------------------- /// /// Start an EOS Auth Login with the passed in LoginOptions. Call this instead of the method on EOSAuthInterface to ensure that @@ -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; + } + + /// + /// 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. + /// + 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