fix(auth): SSO confirmation works in the desktop app; allow switching to SSO
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 1m50s
CI / Build & Quality Checks (pull_request) Successful in 3m15s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Successful in 18m29s

Reported by a desktop user: signing out other sessions with SSO looped.
Server logs: the SSO step completed every time, but each delete_devices
retry came without the session (a fresh 401 challenge), 4 attempts, none
succeeded. The app is the Windows desktop build (WebView2) and the SSO
pages opened in the system browser.

- SSOStage only offered "Continue" once window.open returned a window. The
  desktop app opens the page in the system browser (no window back), and a
  popup can lose its opener, so "authDone" never arrives and the only way
  on was starting over. Now "Continue" (plus "Open the sign-in page again")
  appears as soon as the page was opened; authDone still auto-continues.
- ActionUIA: when the server offers password and SSO, the first flow was
  forced (often password, which an SSO-only account may not know). Offer
  "Use SSO instead" / "Use your password instead".

e2e: desktop (window.open returns null) completes with the session; with
password offered first, SSO is one click away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
Lotus CI
2026-10-26 11:49:52 -04:00
co-authored by Claude Opus 5.5
parent 71b7619082
commit f7f6059b33
3 changed files with 136 additions and 10 deletions
+30 -3
View File
@@ -1,4 +1,5 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useState } from 'react';
import { Box, Button, Text } from 'folds';
import { AuthDict, AuthType, IAuthData, UIAFlow } from 'matrix-js-sdk';
import { getUIAFlowForStages } from '../utils/matrix-uia';
import { useSupportedUIAFlows, useUIACompleted, useUIAFlow } from '../hooks/useUIAFlows';
@@ -23,7 +24,18 @@ type ActionUIAProps = {
export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIAProps) {
const mx = useMatrixClient();
const completed = useUIACompleted(authData);
const { getStageToComplete } = useUIAFlow(authData, ongoingFlow);
// The server may offer several ways to confirm (e.g. password or SSO). Start
// with the given one but let the user switch: an account that signs in with
// SSO may have no password it knows.
const supportedFlows = useSupportedUIAFlows(authData.flows ?? [], SUPPORTED_IN_APP_UIA_STAGES);
const [flow, setFlow] = useState<UIAFlow>(ongoingFlow);
const { getStageToComplete } = useUIAFlow(authData, flow);
const other = supportedFlows.find((f) => f.stages.join() !== flow.stages.join());
const otherLabel = other?.stages.includes(AuthType.Sso)
? 'Use SSO instead'
: other?.stages.includes(AuthType.Password)
? 'Use your password instead'
: undefined;
const stageToComplete = getStageToComplete();
@@ -31,7 +43,7 @@ export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIA
return (
<UIAFlowOverlay
currentStep={completed.length + 1}
stepCount={ongoingFlow.stages.length}
stepCount={flow.stages.length}
onCancel={onCancel}
>
{stageToComplete.type === AuthType.Password && (
@@ -50,6 +62,21 @@ export function ActionUIA({ authData, ongoingFlow, action, onCancel }: ActionUIA
submitAuthDict={action}
/>
)}
{other && otherLabel && completed.length === 0 && (
<Box justifyContent="Center" style={{ marginTop: 8 }}>
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
onClick={() => setFlow(other)}
>
<Text as="span" size="B300">
{otherLabel}
</Text>
</Button>
</Box>
)}
</UIAFlowOverlay>
);
}
+23 -7
View File
@@ -12,6 +12,12 @@ export function SSOStage({
}) {
const { errorCode, error, session } = stageData;
const [ssoWindow, setSSOWindow] = useState<Window>();
// Whether the SSO page was opened. Not the same as having a window handle:
// the desktop app opens it in the system browser and gets none back, and a
// popup can lose its link to us (cross-origin opener policy on the identity
// provider). In both cases "authDone" never arrives, so "Continue" must still
// be offered once the page was opened, or the only way on is starting over.
const [started, setStarted] = useState(false);
const handleSubmit = useCallback(() => {
submitAuthDict({
@@ -22,6 +28,7 @@ export function SSOStage({
const handleContinue = () => {
const w = window.open(ssoRedirectURL, '_blank');
setSSOWindow(w ?? undefined);
setStarted(true);
};
useEffect(() => {
@@ -74,7 +81,9 @@ export function SSOStage({
gap="400"
>
<Text size="T200">
To perform this action you need to authenticate yourself by SSO login.
{started
? 'Finish signing in on the page that opened, then come back here and select Continue.'
: 'To do this, confirm it’s you by signing in with SSO.'}
</Text>
{errorCode && (
<Box alignItems="Center" gap="100" style={{ color: color.Critical.Main }}>
@@ -85,12 +94,19 @@ export function SSOStage({
</Box>
)}
{ssoWindow ? (
<Button variant="Primary" onClick={handleSubmit}>
<Text as="span" size="B400">
Continue
</Text>
</Button>
{started ? (
<Box direction="Column" gap="200">
<Button variant="Primary" onClick={handleSubmit}>
<Text as="span" size="B400">
Continue
</Text>
</Button>
<Button variant="Secondary" fill="None" onClick={handleContinue}>
<Text as="span" size="B400">
Open the sign-in page again
</Text>
</Button>
</Box>
) : (
<Button variant="Primary" onClick={handleContinue}>
<Text as="span" size="B400">