From 53efa395c6b99988f13ccff6c607888d38efdede Mon Sep 17 00:00:00 2001 From: Jskt02 Date: Tue, 17 Feb 2026 13:41:45 -0600 Subject: [PATCH 1/4] Stabilize EOS auth token refresh and P2P connections (EOSU-404) - Added Connect AuthExpiration notify registration with minimal diagnostic logging - Cached the last Connect LoginOptions and credential type to support safe re-auth attempts - Implemented PUID change detection to log when the ProductUserId remains stable or changes mid-session - Introduced a reauth-in-progress guard to prevent concurrent Connect.Login storms during token refresh - Added a debug hook to force a Connect re-login using a fresh Epic IdToken (CopyIdToken -> Connect.Login) --- .../Runtime/Core/EOSManager.cs | 218 ++++++++++++++++-- 1 file changed, 195 insertions(+), 23 deletions(-) diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index fa61b2fa8..2c8cd163c 100644 --- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs +++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs @@ -197,6 +197,20 @@ 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 readonly object s_connectReauthLock = new object(); + static private bool s_connectReauthInProgress = false; + + // IMPORTANT: LoginOptions is a struct, so store it as nullable + static private Epic.OnlineServices.Connect.LoginOptions? s_lastConnectLoginOptions; + + // Cache last used credential type + static private Epic.OnlineServices.ExternalCredentialType s_lastConnectCredentialType + = Epic.OnlineServices.ExternalCredentialType.EpicIdToken; + + // Optional: keep last successful PUID so we can detect mid-session changes. + static private ProductUserId s_lastKnownProductUserId; + // Setting it twice will cause an exception static bool hasSetLoggingCallback; @@ -1163,26 +1177,53 @@ public void StartConnectLoginWithOptions(ExternalCredentialType externalCredenti public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions, OnConnectLoginCallback onloginCallback) { + Debug.Log($"[EOS][ConnectLogin] t={Time.realtimeSinceStartup:F1} puid(prev)={s_lastKnownProductUserId} cred={s_lastConnectCredentialType}"); + // Cache last options for safe re-auth + CacheLastConnectLoginOptions(connectLoginOptions); + 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); + OnConnectLogin?.Invoke(connectLoginData); + onloginCallback?.Invoke(connectLoginData); + return; + } - if (connectLoginData.LocalUserId != null) - { - SetLocalProductUserId(connectLoginData.LocalUserId); + if (connectLoginData.LocalUserId == null) + { + Log("Connect login succeeded but LocalUserId is null (unexpected).", LogType.Error); + OnConnectLogin?.Invoke(connectLoginData); + return; + } + + var newPuid = connectLoginData.LocalUserId; + + if (s_lastKnownProductUserId != null && !s_lastKnownProductUserId.Equals(newPuid)) + { + Debug.LogError( + $"[EOS][PUID] changed {s_lastKnownProductUserId} => {newPuid}. " + + "This can break P2P socket mapping. Refusing to overwrite silently."); + OnConnectLogin?.Invoke(connectLoginData); + return; + } + + Debug.Log($"[EOS][PUID] unchanged {newPuid}"); + s_lastKnownProductUserId = newPuid; + + SetLocalProductUserId(newPuid); ConfigureConnectStatusCallback(); - ConfigureConnectExpirationCallback(connectLoginOptions); + ConfigureConnectExpirationCallback(); OnConnectLogin?.Invoke(connectLoginData); } - - if (onloginCallback != null) + finally { - onloginCallback(connectLoginData); + onloginCallback?.Invoke(connectLoginData); } }); } @@ -1345,25 +1386,29 @@ 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; + + var EOSConnectInterface = GetEOSConnectInterface(); + var addNotifyAuthExpirationOptions = new AddNotifyAuthExpirationOptions(); - s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle => + ulong callbackHandle = EOSConnectInterface.AddNotifyAuthExpiration( + ref addNotifyAuthExpirationOptions, null, + (ref AuthExpirationCallbackInfo callbackInfo) => { - GetEOSConnectInterface()?.RemoveNotifyAuthExpiration(handle); + OnConnectAuthExpiration(ref callbackInfo); }); - } + Debug.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 +1837,130 @@ private static void UpdateNetworkStatus() platformSpecifics?.UpdateNetworkStatus(); } + + private void CacheLastConnectLoginOptions(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions) + { + // LoginOptions is a struct: cannot be null. + // Credentials is nullable: must validate HasValue. + if (!connectLoginOptions.Credentials.HasValue) + return; + + var creds = connectLoginOptions.Credentials.Value; + + s_lastConnectCredentialType = creds.Type; + + // Store a copy (LoginOptions is struct anyway) + s_lastConnectLoginOptions = new Epic.OnlineServices.Connect.LoginOptions + { + Credentials = creds, + UserLoginInfo = connectLoginOptions.UserLoginInfo + }; + } + + + private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo) + { + Debug.Log($"[EOS][AuthExp] fired t={Time.realtimeSinceStartup:F1} puid={s_lastKnownProductUserId} cred={s_lastConnectCredentialType}"); + lock (s_connectReauthLock) + { + if (s_connectReauthInProgress) + { + Debug.Log("[EOS][AuthExp] ignored (reauth already in progress)"); + Log($"{nameof(EOSManager)} Connect auth expiration ignored (reauth already in progress).", LogType.Warning); + return; + } + s_connectReauthInProgress = true; + } + + try + { + Log($"{nameof(EOSManager)} Connect auth expiration received. CredentialType={s_lastConnectCredentialType}.", LogType.Warning); + + 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 + { + lock (s_connectReauthLock) + { + s_connectReauthInProgress = false; + } + } + } + + + private void RefreshConnectLoginWithFreshEpicIdToken() + { + var authInterface = GetEOSAuthInterface(); + var connectInterface = GetEOSConnectInterface(); + + if (authInterface == null || connectInterface == null) + { + Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Auth/Connect interface is null."); + return; + } + + var localEpicUserId = GetLocalUserId(); + if (localEpicUserId == null) + { + Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Local EpicAccountId is null."); + return; + } + + var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions + { + AccountId = localEpicUserId + }; + + Epic.OnlineServices.Auth.IdToken? idToken; + var copyResult = authInterface.CopyIdToken(ref copyIdTokenOptions, out idToken); + + if (copyResult != Result.Success || !idToken.HasValue) + { + Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: CopyIdToken failed with {copyResult}."); + return; + } + + var jwt = idToken.Value.JsonWebToken; + if (string.IsNullOrEmpty(jwt)) + { + Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: JWT is empty."); + return; + } + + // Reuse cached UserLoginInfo if present + Epic.OnlineServices.Connect.UserLoginInfo? cachedUserLoginInfo = null; + if (s_lastConnectLoginOptions.HasValue) + cachedUserLoginInfo = s_lastConnectLoginOptions.Value.UserLoginInfo; + + var refreshedLoginOptions = new Epic.OnlineServices.Connect.LoginOptions + { + Credentials = new Epic.OnlineServices.Connect.Credentials + { + Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken, + Token = jwt + }, + UserLoginInfo = cachedUserLoginInfo + }; + + Log($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Re-logging Connect with fresh JWT.", LogType.Warning); + + StartConnectLoginWithOptions(refreshedLoginOptions, null); + } + + public void Debug_RefreshConnectLoginWithFreshEpicIdToken() + { + Debug.Log("[EOS][ReauthTest] Manual reauth triggered"); + RefreshConnectLoginWithFreshEpicIdToken(); + } } #endif @@ -1832,6 +2001,9 @@ static public EOSSingleton Instance /// Calls Init() /// /// + /// + + void Awake() { // If there's already been an EOSManager, From 7da495b610b61e2020ed909f2c4414e734deacb9 Mon Sep 17 00:00:00 2001 From: Jskt02 Date: Fri, 20 Feb 2026 18:48:20 -0600 Subject: [PATCH 2/4] Harden Epic auth logging and login flow - Updated new diagnostics to use the Log() wrapper instead of Debug.Log - Moved OnConnectLogin call into the finally block - Prevented onLoginCallback from being called twice - Removed Debug_RefreshConnectLoginWithFreshEpicIdToken() debug helper - Fixed variable name for better readability and consistency - Added handling for auth-expiration: if credential type isn't EpicIdToken, log warning and require game to re-fetch the token. - Added safety check to prevent silently overwriting the local PUID if a different ProductUserId is returned. - Reduced duplication by making RefreshConnectLoginWithFreshEpicIdToken() reuse the StartConnectLoginWithEpicAccount() flow where applicable. --- .../Runtime/Core/EOSManager.cs | 125 ++++++------------ 1 file changed, 39 insertions(+), 86 deletions(-) diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index 2c8cd163c..2b117074b 100644 --- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs +++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs @@ -198,7 +198,9 @@ public partial class EOSSingleton static private NotifyEventHandle s_notifyConnectAuthExpirationCallbackHandle; // --- Connect reauth hardening (prevents PUID churn / P2P route resets) --- - static private readonly object s_connectReauthLock = new object(); + // NOTE: EOS callbacks are expected to run on the same thread that drives PlatformInterface.Tick(). + // In typical Unity integrations that's the main thread. We keep this as a simple guard to avoid + // reauth/login storms during token refresh. static private bool s_connectReauthInProgress = false; // IMPORTANT: LoginOptions is a struct, so store it as nullable @@ -270,8 +272,7 @@ private string PUIDToString(ProductUserId puid) /// protected void SetLocalProductUserId(ProductUserId localProductUserId) { - Log("Changing PUID: " + PUIDToString(s_localProductUserId) + " => " + - PUIDToString(localProductUserId)); + Log("Local ProductUserId changed."); s_localProductUserId = localProductUserId; } @@ -1090,7 +1091,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 @@ -1177,7 +1178,8 @@ public void StartConnectLoginWithOptions(ExternalCredentialType externalCredenti public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions, OnConnectLoginCallback onloginCallback) { - Debug.Log($"[EOS][ConnectLogin] t={Time.realtimeSinceStartup:F1} puid(prev)={s_lastKnownProductUserId} cred={s_lastConnectCredentialType}"); + // Do not log user identifiers (PUID/EAS ids). + Log($"[EOS][ConnectLogin] start cred={s_lastConnectCredentialType}"); // Cache last options for safe re-auth CacheLastConnectLoginOptions(connectLoginOptions); @@ -1191,7 +1193,6 @@ public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOption { Log($"Connect login was not successful. ResultCode: {connectLoginData.ResultCode}", LogType.Error); OnConnectLogin?.Invoke(connectLoginData); - onloginCallback?.Invoke(connectLoginData); return; } @@ -1206,14 +1207,15 @@ public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOption if (s_lastKnownProductUserId != null && !s_lastKnownProductUserId.Equals(newPuid)) { - Debug.LogError( - $"[EOS][PUID] changed {s_lastKnownProductUserId} => {newPuid}. " + - "This can break P2P socket mapping. Refusing to overwrite silently."); + Log( + "[EOS][PUID] Local ProductUserId changed mid-session. " + + "This can break P2P socket mapping; refusing to overwrite silently.", + LogType.Error); OnConnectLogin?.Invoke(connectLoginData); return; } - Debug.Log($"[EOS][PUID] unchanged {newPuid}"); + Log("[EOS][PUID] unchanged (stable across Connect.Login)."); s_lastKnownProductUserId = newPuid; SetLocalProductUserId(newPuid); @@ -1305,7 +1307,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 @@ -1391,16 +1393,16 @@ private void ConfigureConnectExpirationCallback() if (s_notifyConnectAuthExpirationCallbackHandle != null) return; - var EOSConnectInterface = GetEOSConnectInterface(); + var eosConnectInterface = GetEOSConnectInterface(); var addNotifyAuthExpirationOptions = new AddNotifyAuthExpirationOptions(); - ulong callbackHandle = EOSConnectInterface.AddNotifyAuthExpiration( + ulong callbackHandle = eosConnectInterface.AddNotifyAuthExpiration( ref addNotifyAuthExpirationOptions, null, (ref AuthExpirationCallbackInfo callbackInfo) => { OnConnectAuthExpiration(ref callbackInfo); }); - Debug.Log("[EOS][AuthExp] registered"); + Log("[EOS][AuthExp] registered"); s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle => { GetEOSConnectInterface()?.RemoveNotifyAuthExpiration(handle); @@ -1860,18 +1862,16 @@ private void CacheLastConnectLoginOptions(Epic.OnlineServices.Connect.LoginOptio private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo) { - Debug.Log($"[EOS][AuthExp] fired t={Time.realtimeSinceStartup:F1} puid={s_lastKnownProductUserId} cred={s_lastConnectCredentialType}"); - lock (s_connectReauthLock) + // Do not log user identifiers (PUID/EAS ids). + Log($"[EOS][AuthExp] received. CredentialType={s_lastConnectCredentialType}", LogType.Warning); + if (s_connectReauthInProgress) { - if (s_connectReauthInProgress) - { - Debug.Log("[EOS][AuthExp] ignored (reauth already in progress)"); - Log($"{nameof(EOSManager)} Connect auth expiration ignored (reauth already in progress).", LogType.Warning); - return; - } - s_connectReauthInProgress = true; + Log($"{nameof(EOSManager)} Connect auth expiration ignored (reauth already in progress).", LogType.Warning); + return; } + s_connectReauthInProgress = true; + try { Log($"{nameof(EOSManager)} Connect auth expiration received. CredentialType={s_lastConnectCredentialType}.", LogType.Warning); @@ -1882,84 +1882,40 @@ private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo } else { - Debug.LogWarning( + Log( $"{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."); + "Plugin cannot refresh that token automatically. Game should re-fetch external token and call StartConnectLoginWithOptions again.", + LogType.Warning); } } finally { - lock (s_connectReauthLock) - { - s_connectReauthInProgress = false; - } + s_connectReauthInProgress = false; } } - private void RefreshConnectLoginWithFreshEpicIdToken() { - var authInterface = GetEOSAuthInterface(); - var connectInterface = GetEOSConnectInterface(); - - if (authInterface == null || connectInterface == null) - { - Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Auth/Connect interface is null."); + if (s_connectReauthInProgress) return; - } - var localEpicUserId = GetLocalUserId(); - if (localEpicUserId == null) - { - Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Local EpicAccountId is null."); - return; - } + s_connectReauthInProgress = true; - var copyIdTokenOptions = new Epic.OnlineServices.Auth.CopyIdTokenOptions + try { - AccountId = localEpicUserId - }; - - Epic.OnlineServices.Auth.IdToken? idToken; - var copyResult = authInterface.CopyIdToken(ref copyIdTokenOptions, out idToken); + var localEpicUserId = GetLocalUserId(); + if (localEpicUserId == null) + { + Log($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Local EpicAccountId is null.", LogType.Warning); + return; + } - if (copyResult != Result.Success || !idToken.HasValue) - { - Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: CopyIdToken failed with {copyResult}."); - return; + StartConnectLoginWithEpicAccount(localEpicUserId, null); } - - var jwt = idToken.Value.JsonWebToken; - if (string.IsNullOrEmpty(jwt)) + finally { - Debug.LogError($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: JWT is empty."); - return; + s_connectReauthInProgress = false; } - - // Reuse cached UserLoginInfo if present - Epic.OnlineServices.Connect.UserLoginInfo? cachedUserLoginInfo = null; - if (s_lastConnectLoginOptions.HasValue) - cachedUserLoginInfo = s_lastConnectLoginOptions.Value.UserLoginInfo; - - var refreshedLoginOptions = new Epic.OnlineServices.Connect.LoginOptions - { - Credentials = new Epic.OnlineServices.Connect.Credentials - { - Type = Epic.OnlineServices.ExternalCredentialType.EpicIdToken, - Token = jwt - }, - UserLoginInfo = cachedUserLoginInfo - }; - - Log($"{nameof(EOSManager)} RefreshConnectLoginWithFreshEpicIdToken: Re-logging Connect with fresh JWT.", LogType.Warning); - - StartConnectLoginWithOptions(refreshedLoginOptions, null); - } - - public void Debug_RefreshConnectLoginWithFreshEpicIdToken() - { - Debug.Log("[EOS][ReauthTest] Manual reauth triggered"); - RefreshConnectLoginWithFreshEpicIdToken(); } } #endif @@ -2001,9 +1957,6 @@ static public EOSSingleton Instance /// Calls Init() /// /// - /// - - void Awake() { // If there's already been an EOSManager, From 6ea2cc99972659ff92636c644fb306ef57eae146 Mon Sep 17 00:00:00 2001 From: Jskt02 Date: Tue, 24 Feb 2026 07:46:23 -0600 Subject: [PATCH 3/4] Update EOSManager: guarantee Connect login callbacks and document reauth policy - Always invoke OnConnectLogin / onloginCallback from finally (exactly-once). - Add inline docs for auth-expiration credential policy. - Remove unused caching and minor handler cleanup --- .../Runtime/Core/EOSManager.cs | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index 2b117074b..3142d22d3 100644 --- a/com.playeveryware.eos/Runtime/Core/EOSManager.cs +++ b/com.playeveryware.eos/Runtime/Core/EOSManager.cs @@ -198,17 +198,12 @@ public partial class EOSSingleton static private NotifyEventHandle s_notifyConnectAuthExpirationCallbackHandle; // --- Connect reauth hardening (prevents PUID churn / P2P route resets) --- - // NOTE: EOS callbacks are expected to run on the same thread that drives PlatformInterface.Tick(). - // In typical Unity integrations that's the main thread. We keep this as a simple guard to avoid - // reauth/login storms during token refresh. static private bool s_connectReauthInProgress = false; - // IMPORTANT: LoginOptions is a struct, so store it as nullable - static private Epic.OnlineServices.Connect.LoginOptions? s_lastConnectLoginOptions; + - // Cache last used credential type - static private Epic.OnlineServices.ExternalCredentialType s_lastConnectCredentialType - = Epic.OnlineServices.ExternalCredentialType.EpicIdToken; + // Last Connect credential type used. Used to decide whether we can auto-refresh on auth expiration. + static private Epic.OnlineServices.ExternalCredentialType s_lastConnectCredentialType; // Optional: keep last successful PUID so we can detect mid-session changes. static private ProductUserId s_lastKnownProductUserId; @@ -1178,10 +1173,9 @@ 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}"); - // Cache last options for safe re-auth - CacheLastConnectLoginOptions(connectLoginOptions); var connectInterface = GetEOSPlatformInterface().GetConnectInterface(); connectInterface.Login(ref connectLoginOptions, null, @@ -1192,14 +1186,12 @@ public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOption if (connectLoginData.ResultCode != Result.Success) { Log($"Connect login was not successful. ResultCode: {connectLoginData.ResultCode}", LogType.Error); - OnConnectLogin?.Invoke(connectLoginData); return; } if (connectLoginData.LocalUserId == null) { Log("Connect login succeeded but LocalUserId is null (unexpected).", LogType.Error); - OnConnectLogin?.Invoke(connectLoginData); return; } @@ -1211,7 +1203,6 @@ public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOption "[EOS][PUID] Local ProductUserId changed mid-session. " + "This can break P2P socket mapping; refusing to overwrite silently.", LogType.Error); - OnConnectLogin?.Invoke(connectLoginData); return; } @@ -1221,10 +1212,11 @@ public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOption SetLocalProductUserId(newPuid); ConfigureConnectStatusCallback(); ConfigureConnectExpirationCallback(); - OnConnectLogin?.Invoke(connectLoginData); } finally { + // Always notify listeners/caller exactly once, regardless of early returns above. + OnConnectLogin?.Invoke(connectLoginData); onloginCallback?.Invoke(connectLoginData); } }); @@ -1398,10 +1390,7 @@ private void ConfigureConnectExpirationCallback() ulong callbackHandle = eosConnectInterface.AddNotifyAuthExpiration( ref addNotifyAuthExpirationOptions, null, - (ref AuthExpirationCallbackInfo callbackInfo) => - { - OnConnectAuthExpiration(ref callbackInfo); - }); + OnConnectAuthExpiration); Log("[EOS][AuthExp] registered"); s_notifyConnectAuthExpirationCallbackHandle = new NotifyEventHandle(callbackHandle, handle => { @@ -1840,26 +1829,24 @@ private static void UpdateNetworkStatus() platformSpecifics?.UpdateNetworkStatus(); } - private void CacheLastConnectLoginOptions(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions) + private void CacheLastConnectCredentialType(Epic.OnlineServices.Connect.LoginOptions connectLoginOptions) { - // LoginOptions is a struct: cannot be null. // Credentials is nullable: must validate HasValue. if (!connectLoginOptions.Credentials.HasValue) return; - var creds = connectLoginOptions.Credentials.Value; - - s_lastConnectCredentialType = creds.Type; - - // Store a copy (LoginOptions is struct anyway) - s_lastConnectLoginOptions = new Epic.OnlineServices.Connect.LoginOptions - { - Credentials = creds, - UserLoginInfo = connectLoginOptions.UserLoginInfo - }; + 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). @@ -1882,10 +1869,9 @@ private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo } else { - Log( + 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.", - LogType.Warning); + "Plugin cannot refresh that token automatically. Game should re-fetch external token and call StartConnectLoginWithOptions again."); } } finally @@ -1896,6 +1882,9 @@ private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo 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. We keep this as a simple guard to avoid + // reauth/login storms during token refresh. if (s_connectReauthInProgress) return; From b268e68c461c9dc9d341ab63acd30f032f0f0ca6 Mon Sep 17 00:00:00 2001 From: Jskt02 Date: Tue, 24 Feb 2026 11:46:42 -0600 Subject: [PATCH 4/4] EOSManager: align PUID handling with expected behavior Removed ignore PUID change early-return so cached LocalProductUserId always stays consistent with the SDK --- .../Runtime/Core/EOSManager.cs | 54 ++++++------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/com.playeveryware.eos/Runtime/Core/EOSManager.cs b/com.playeveryware.eos/Runtime/Core/EOSManager.cs index 3142d22d3..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; @@ -198,16 +199,9 @@ public partial class EOSSingleton static private NotifyEventHandle s_notifyConnectAuthExpirationCallbackHandle; // --- Connect reauth hardening (prevents PUID churn / P2P route resets) --- - static private bool s_connectReauthInProgress = false; - - - - // Last Connect credential type used. Used to decide whether we can auto-refresh on auth expiration. + 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; - // Optional: keep last successful PUID so we can detect mid-session changes. - static private ProductUserId s_lastKnownProductUserId; - // Setting it twice will cause an exception static bool hasSetLoggingCallback; @@ -267,8 +261,14 @@ private string PUIDToString(ProductUserId puid) /// protected void SetLocalProductUserId(ProductUserId localProductUserId) { - Log("Local ProductUserId changed."); + 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); + } } //------------------------------------------------------------------------- @@ -1196,19 +1196,6 @@ public void StartConnectLoginWithOptions(Epic.OnlineServices.Connect.LoginOption } var newPuid = connectLoginData.LocalUserId; - - if (s_lastKnownProductUserId != null && !s_lastKnownProductUserId.Equals(newPuid)) - { - Log( - "[EOS][PUID] Local ProductUserId changed mid-session. " + - "This can break P2P socket mapping; refusing to overwrite silently.", - LogType.Error); - return; - } - - Log("[EOS][PUID] unchanged (stable across Connect.Login)."); - s_lastKnownProductUserId = newPuid; - SetLocalProductUserId(newPuid); ConfigureConnectStatusCallback(); ConfigureConnectExpirationCallback(); @@ -1851,18 +1838,16 @@ private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo { // Do not log user identifiers (PUID/EAS ids). Log($"[EOS][AuthExp] received. CredentialType={s_lastConnectCredentialType}", LogType.Warning); - if (s_connectReauthInProgress) + + // 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; } - s_connectReauthInProgress = true; - try { - Log($"{nameof(EOSManager)} Connect auth expiration received. CredentialType={s_lastConnectCredentialType}.", LogType.Warning); - if (s_lastConnectCredentialType == Epic.OnlineServices.ExternalCredentialType.EpicIdToken) { RefreshConnectLoginWithFreshEpicIdToken(); @@ -1876,20 +1861,15 @@ private void OnConnectAuthExpiration(ref AuthExpirationCallbackInfo callbackInfo } finally { - s_connectReauthInProgress = false; + 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. We keep this as a simple guard to avoid - // reauth/login storms during token refresh. - if (s_connectReauthInProgress) - return; - - s_connectReauthInProgress = true; - + // In typical Unity integrations that's the main thread. try { var localEpicUserId = GetLocalUserId(); @@ -1901,9 +1881,9 @@ private void RefreshConnectLoginWithFreshEpicIdToken() StartConnectLoginWithEpicAccount(localEpicUserId, null); } - finally + catch(System.Exception ex) { - s_connectReauthInProgress = false; + Log($"{nameof(EOSManager)} {nameof(RefreshConnectLoginWithFreshEpicIdToken)} failed:{ex.GetType().Name}: {ex.Message}",LogType.Error); } } }