-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Clear SAML short-lived-token guard on the no-JSON early-return path (based on #94082) #95017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
feb15e0
afeead7
f9660aa
7e88ae4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ import Log from '@libs/Log'; | |
| import {handleSAMLLoginError, postSAMLLogin} from '@libs/LoginUtils'; | ||
| import Navigation from '@libs/Navigation/Navigation'; | ||
|
|
||
| import {clearSignInData, setAccountError, signInWithShortLivedAuthToken} from '@userActions/Session'; | ||
| import {clearSignInData, setAccountError, setIsAuthenticatingWithShortLivedToken, signInWithShortLivedAuthToken} from '@userActions/Session'; | ||
|
|
||
| import CONFIG from '@src/CONFIG'; | ||
| import CONST from '@src/CONST'; | ||
|
|
@@ -32,6 +32,8 @@ function SAMLSignInPage() { | |
| const hasOpenedAuthSession = useRef(false); | ||
|
|
||
| const handleExitSAMLFlow = useCallback(() => { | ||
| // Clear the guard we set before opening the in-app browser so we don't block future reauthentication | ||
| setIsAuthenticatingWithShortLivedToken(false); | ||
| Navigation.isNavigationReady().then(() => { | ||
| Navigation.goBack(); | ||
| clearSignInData(); | ||
|
|
@@ -51,20 +53,19 @@ function SAMLSignInPage() { | |
| const searchParams = new URLSearchParams(new URL(url).search); | ||
| const jsonParam = searchParams.get('json'); | ||
|
|
||
| if (!jsonParam) { | ||
| Log.hmmm('SAMLSignInPage - No JSON parameter found in callback URL'); | ||
| return; | ||
| } | ||
|
|
||
| let shortLivedAuthToken: string | null = null; | ||
| try { | ||
| const decodedData = JSON.parse(jsonParam) as Record<string, string | null>; | ||
| shortLivedAuthToken = decodedData.shortLivedAuthToken ?? null; | ||
| if (decodedData.error) { | ||
| Log.hmmm('SAMLSignInPage - SAML login returned error', {error: decodedData.error}); | ||
| if (jsonParam) { | ||
| try { | ||
| const decodedData = JSON.parse(jsonParam) as Record<string, string | null>; | ||
| shortLivedAuthToken = decodedData.shortLivedAuthToken ?? null; | ||
| if (decodedData.error) { | ||
| Log.hmmm('SAMLSignInPage - SAML login returned error', {error: decodedData.error}); | ||
| } | ||
| } catch (parseError) { | ||
| Log.hmmm('SAMLSignInPage - Failed to parse JSON parameter', {error: parseError}); | ||
| } | ||
| } catch (parseError) { | ||
| Log.hmmm('SAMLSignInPage - Failed to parse JSON parameter', {error: parseError}); | ||
| } else { | ||
| Log.hmmm('SAMLSignInPage - No JSON parameter found in callback URL'); | ||
| } | ||
|
|
||
| if (!account?.isLoading && credentials?.login && shortLivedAuthToken) { | ||
|
|
@@ -73,6 +74,11 @@ function SAMLSignInPage() { | |
| return; | ||
| } | ||
|
|
||
| // The browser returned but we couldn't sign in (no JSON parameter, a parse failure, or no token), so clear | ||
| // the guard we set before opening it and send the user back to a clean state. Otherwise the guard stays | ||
| // true, and since loginCallback URLs hide the back button and leave account.isLoading true, the user gets | ||
| // stuck on the loading screen with future reauthenticate() calls aborting. | ||
| setIsAuthenticatingWithShortLivedToken(false); | ||
| clearSignInData(); | ||
| setAccountError(translate('common.error.login')); | ||
| Navigation.isNavigationReady().then(() => { | ||
|
|
@@ -90,6 +96,12 @@ function SAMLSignInPage() { | |
| return; | ||
| } | ||
| hasOpenedAuthSession.current = true; | ||
| // Opening the in-app browser backgrounds the app. When it returns, the app resumes and fires | ||
| // reconnectApp() with the expired authToken, which 407s and triggers reauthenticate() -> 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. signInWithShortLivedAuthToken() resets it on | ||
| // success; the cancel/error/failure paths reset it via handleExitSAMLFlow and handleNavigationStateChange. | ||
|
Comment on lines
+99
to
+103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB these comments are too long |
||
| setIsAuthenticatingWithShortLivedToken(true); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the SAML resume race this guard needs to be visible to Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Codex bot describes the mechanism correctly but the race it predicts can't actually happen here, so no change is needed. Why it's safe: The dangerous window the bot worries about — browser backgrounds the app → app resumes → On the authToken analogy: the "manually mirrored" example the bot cites ( Confidence & caveatHigh confidence on the JS event-loop ordering. There's no synchronous path from |
||
| openAuthSessionAsync(SAMLUrl, CONST.SAML_REDIRECT_URL) | ||
| .then((response: WebBrowserAuthSessionResult) => { | ||
| if (response.type !== 'success') { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when should we use this vs
clearShortLivedAuthState? @thelullabyyThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MelvinBot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Short answer:
setIsAuthenticatingWithShortLivedToken(false)is correct here — it's not interchangeable withclearShortLivedAuthState.They differ in scope:
setIsAuthenticatingWithShortLivedToken(false)flips only the RAM-only guardRAM_ONLY_IS_AUTHENTICATING_WITH_SHORT_LIVED_TOKEN.clearShortLivedAuthState()flips that guard and wipes theSESSIONshort-lived fields (signedInWithShortLivedAuthToken,isSupportAuthTokenUsed), returning a promise.This failure path only ever set the guard (right before opening the browser) and never reached
signInWithShortLivedAuthToken— so theSESSIONshort-lived fields were never populated by this flow and there's nothing else to clear. The setter is the exact inverse of what we set, matchinghandleExitSAMLFlow.clearShortLivedAuthState()is the heavier recovery helper thatreauthenticate()uses when it detects a stale guard after a real short-lived sign-in may have populatedSESSION— there it needs to wipe the whole state, not just the guard. We don't need that reach here, so I'd keep the minimal setter.Separately, I've merged
maininto the branch (the only conflict was the reformatted import block, which I resolved).