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
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:
co-authored by
Claude Opus 5.5
parent
71b7619082
commit
f7f6059b33
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user