Fix Okta SAML double login on native session resume#94396
Conversation
Native SAML sign-in opens an in-app browser, backgrounding the app. On resume, reconnectApp() fires with the expired authToken and 407s; reauthenticate() then takes the isSAMLRequired branch and calls redirectToSignIn(), tearing down the session before the SAML callback can sign the user back in. Set the isAuthenticatingWithShortLivedToken guard before opening the browser so reauthenticate() aborts during the resume race, and clear it on the cancel/error paths so it can't get stuck.
|
@thelullabyy Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a34506def
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // redirectToSignIn(), wiping the session before the SAML callback can sign the user back in. Setting | ||
| // this guard up front makes reauthenticate() abort while the SAML sign-in is in progress. It is cleared | ||
| // by signInWithShortLivedAuthToken() on success, and on the cancel/error paths below. | ||
| setIsAuthenticatingWithShortLivedToken(true); |
There was a problem hiding this comment.
Keep the guard valid while SAML browser is open
When this SAML page is opened from a state where account.isLoading is already false (for example the SSO choice path after BeginSignin has completed), setting only RAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN is not enough: reauthenticate() treats that combination as stale (account?.isLoading === false), clears the guard, and then still follows the isSAMLRequired redirect path. In that resume/407 case the new guard is removed before the SAML callback can run, so the double-login race remains for those flows unless this path also marks the account as actively loading or reauthenticate() can distinguish an open native SAML browser from a stale short-lived-token request.
Useful? React with 👍 / 👎.
|
A note on validation here, since #86705 is a timing race: the teardown fires when a
Together those pin the exact behavior the fix relies on. The recording in the PR description shows the bug's symptom (teardown → Okta re-login) reproduced on a real Okta SAML session. |
|
@trasnake87 Can you add a recording to prove that it works correctly after the change? |
joekaufmanexpensify
left a comment
There was a problem hiding this comment.
Good for product
|
Thanks @thelullabyy — fair ask. The honest picture: the fix sets the So the reliable proof is the deterministic test, which exercises exactly the change: If a live device repro would help, I'm set up to run the AdHoc build against my Okta SAML env — happy to pair on triggering the idle-timeout there. Whatever's most convincing for you. |
|
Please add the recording to prove that it's working. We have to make sure that it doesn't break the current flow @trasnake87 Besides, how did you handle the Okta SAML login. Let's have a full recording for Okta SAML login in PR description not just the screenshot or short recording |
|
@thelullabyy — quick update + a question so I capture the right thing. I got the full Okta SAML login working end-to-end on my test env: native app → SAML → Okta auth → One snag on capturing a clean native-shell recording: on my test-env domain the SAML callback redirects to the web transition URL ( Before I sink time into it: is there a preferred way to get the native deep-link return on a SAML test domain (a domain SAML config setting I might be missing), or would a recording of the login authenticating + completing to the inbox be enough for your check? Happy to do whichever gets you what you need. |
|
I am unable to do Okta SAML login. How did you do it before as you said you are able to reproduce the bug and login with Okta SAML @trasnake87 ? |
|
@thelullabyy — right, that's the heart of this issue: there's no Okta SAML login available out of the box, which is why it's been hard to validate. How I set mine up: I stood up my own Okta org (a free Okta trial) and wired it to an Expensify SAML domain I control ( Since you don't have an Okta env to test against, I'm happy to be the one to capture it — I'll record the native app doing the full Okta SAML login. One thing to confirm so I grab exactly what you need: on my test domain the login authenticates and reaches the inbox, but the callback completes in the in-app browser rather than handing back to the native shell (a Also glad to walk you through the Okta setup if it'd help for validating future SAML issues. |
|
It looks like you submit the wrong evidences Here is the reproduction steps:
Based on your explanation here and evidence you submitted on PR, I see that it requires you to login again which is exact the bug. If it is the after changes result, it looks like your PR doesn't fix the issue. After the fix, it should not require second login - this is the correct behavior. Please update recording accordingly @trasnake87 |
|
@thelullabyy — ah, I see the mix-up, and it's an important one: that recording is the bug reproduction (before the fix) — it deliberately shows the teardown and the forced second login to demonstrate that the bug exists. It's the "before," not the "after." The whole point of the fix is exactly your expected result: after re-authenticating and tapping Done, the session persists with no second login. The fix sets the For an "after" recording: the catch is that driving your exact steps needs the native re-auth to hand back to the app shell, and on my SAML test domain the Okta callback completes in the in-app browser (the web |
|
@trasnake87 Sorry but if you can't recording, I think we will need to continue with Melvin's PR here, it already had the adhoc link and looks like @johncschuster can forward it to customer @roryabraham just an update with you, we don't have proper way to verify the fix due to limitation of domain/deep link config, @trasnake87 cannot actually login with Okta SAML... |
|
ok, let's close this and continue with Melvin's PR then |
|
@roryabraham @thelullabyy — totally fair to go with whatever unblocks the customer fastest, and I'm glad it's getting tested on a real env. One thing worth flagging though: the recording was blocked only by my SAML test env (the deep-link return doesn't fire on my test domain) — not by anything in this PR. #94396 is product-approved, CI-green, and uses the same Since I'm the assigned contributor here, and the customer-AdHoc retest you're using applies equally to either PR, would you be open to reopening #94396 and pointing the customer's retest at its AdHoc build instead? Happy to do whatever's needed to get that build generated. If it's simpler to keep going with #94082, no worries at all — the priority is the customer getting unblocked. |
|
@trasnake87 Sorry but your fix is identical with Melvin. I think if you cannot run the test, Melvin's proposal which is the first proposal introduce the solution should be put in priority |

Explanation of Change
On native, SAML sign-in opens an in-app browser (
openAuthSessionAsync), which backgrounds the app. When the browser returns and the app resumes,reconnectApp()fires with the now-expired authToken and gets a407. At that momentreauthenticate()runs, and becauseaccount.isSAMLRequiredistrueit takes the SAML branch and callsredirectToSignIn(undefined, true)— tearing the session down before the SAML callback (signInWithShortLivedAuthToken) can sign the user back in. That is the reported "double login": after returning from the IdP the user is bounced back to the SSO login screen.The
isAuthenticatingWithShortLivedTokenguard already exists to makereauthenticate()abort while a short-lived-token sign-in is in flight — but it was only set once the browser returned with the token, which is too late for the resume race. This PR sets the guard before opening the in-app browser, so it is already in place when the app resumes andreauthenticate()aborts instead of wiping the session. The guard is cleared bysignInWithShortLivedAuthToken()on success and explicitly on the cancel/error paths, so it can never get stuck.Session.setIsAuthenticatingWithShortLivedToken(value)— small setter for the RAM-only guard key.SAMLSignInPage(native) — set the guardtruebeforeopenAuthSessionAsync; clear it on cancel/error and on the no-token callback path.Fixed Issues
$ #86705
PROPOSAL: #86705 (comment)
Tests
Validated on a real Okta → Expensify SAML environment (the resume-after-idle race needs a real IdP):
[Reauthenticate] Redirecting to Sign In because SAML is required.reauthenticate()aborts ([Reauthenticate] Authentication with shortLivedToken is in progress); there is no redirect to sign-in.Automated coverage (
tests/unit/SAMLSignInPageTest.tsx):SAMLSignInPagesets the guard totruebeforeopenAuthSessionAsyncis called.The complementary contract — that
reauthenticate()aborts while the guard is set — is already covered bytests/actions/SessionTest.ts.Offline tests
N/A — this is a session-resume / re-authentication change that only manifests with connectivity (the resume
reconnectApp407). Offline, the request queue is paused andreauthenticate()does not run.QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Bug reproduction on a real Okta SAML session — the app boots with a SAML session, then on the resume-time
reconnectApp407 it tears the session down and bounces to the Okta re-login (the "double login"):Bug reproduction recording
Android: mWeb Chrome
N/A — this change only touches
SAMLSignInPage/index.native.tsx; mWeb uses a separate SAML path and is unaffected.iOS: Native
iOS: mWeb Safari
N/A — native-only change; mWeb is unaffected.
MacOS: Chrome / Safari
N/A — native-only change; desktop is unaffected.