Related user story
#47116
Task
Teaches the My device page (DeviceUserPage) the SSO dance: when a device API call returns the SSO-required 401, initiate device SSO and navigate to the IdP; after the callback redirects back with the session cookie, the page loads normally. Handles expired sessions (mid-visit 401 → re-initiate) and SSO failure states. No Figma exists for this surface (spec open question 5) — states below are the proposed minimum; confirm with product before polishing.
Context for a fresh session: read tmp/47116/spec.md (spec artifacts in the spec lead's local checkout; gitignored) ("Device SSO session and cookies" narrative). Contracts from sub-issues 2/3:
POST /api/_version_/fleet/device/{token}/sso → {"url": "<IdP URL>"} + handshake cookie; SSO-exempt, device-token-authenticated.
- Gated endpoints return HTTP 401 with
"sso_required": true in the JSON body when the setting is on and no valid session cookie accompanies the token. A plain 401 (no marker) still means invalid/expired token → existing error page.
- After IdP auth, the server 303s the browser back to
/device/{token}; the session cookie is set server-side — the SPA never touches it.
Detect SSO-required
frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx — the page already special-cases auth failures: isAuthenticationError derived from the device-details query error at :360-361, rendered via DeviceUserError (frontend/components/DeviceUserError/DeviceUserError.tsx:48-64, "This URL is invalid or expired."). Bonus signal: during Setup Experience, orbit opens the page with ?setup_only=1 (orbit/pkg/setup_experience/setup_experience.go:187) — the server-side carve-out (sub-issue 3) makes SSO a non-issue there, but the SPA can use the param as defense in depth to avoid ever auto-initiating in that mode. Extend the error classification: parse the API error body for sso_required: true (check how services/entities/device_user.ts surfaces error bodies — follow the existing isDupDetailsError handling) and branch:
sso_required → start the SSO flow (below) instead of rendering the invalid-URL error.
- 401 without the marker → existing invalid-URL error, unchanged.
Apply the same classification to the other device queries the page issues (policies, software, certificates) so a mid-visit session expiry re-initiates rather than erroring — centralize in one helper rather than per-query copies.
Initiate and redirect
Add the route to frontend/utilities/endpoints.ts (it is not there yet), then the service method in frontend/services/entities/device_user.ts. Note the existing methods there build URLs from DEVICE_USER_DETAILS as a string prefix (device_user.ts:71-89, e.g. `${DEVICE_USER_DETAILS}/${token}/refetch`) — follow that pattern:
initiateDeviceSSO: (deviceAuthToken: string) => {
const { DEVICE_USER_DETAILS } = endpoints;
return sendRequest("POST", `${DEVICE_USER_DETAILS}/${deviceAuthToken}/sso`);
},
Flow component/state on DeviceUserPage:
- On sso_required: call
initiateDeviceSSO(deviceAuthToken), then window.location.href = response.url — same pattern as frontend/pages/MDMAppleSSOPage/MDMAppleSSOPage.tsx (calls initiate, navigates to the returned IdP URL).
- While initiating/navigating: render a lightweight "Redirecting to your organization's sign-in page…" state (Spinner + text; reuse the page's existing loading treatment). Avoid flashing the full page skeleton first — classify before first render where practical.
- Loop guard: if the page loads, initiates, comes back, and still gets sso_required (cookie blocked, third-party-cookie policies, clock skew), do not re-initiate infinitely. Track one automatic attempt (e.g. sessionStorage flag keyed by token) and on the second consecutive failure render a terminal error state with a "Sign in again" button for manual retry.
- Initiate-call failures (network, 422 feature-off race, free tier) → terminal error state with retry; copy TBD with product (open question 5), placeholder: "Your organization requires single sign-on to view this page. We couldn't start sign-in — try again or contact your IT admin."
Return leg
Nothing to build: the server 303s back to /device/{token} and the queries just succeed. Verify the router treats the return URL identically (device routes at frontend/router/index.tsx:457-466, outside the authenticated/unauthenticated wrappers) and that any sub-route the user started on (/device/{token}/self-service, /software, /policies — frontend/router/paths.ts:187-199) survives the round-trip: the initiate endpoint's originalURL comes from the server; pass the current full path if sub-issue 2 supports it, else landing on the root device page is acceptable v1.
States summary (proposed minimum — no Figma)
| State |
Treatment |
| SSO required, first load |
Auto-initiate + "Redirecting to your organization's sign-in page…" |
| Session expired mid-visit |
Same auto-initiate on the first sso_required response |
| Second consecutive sso_required |
Terminal error card + "Sign in again" button |
| Initiate call fails |
Terminal error card + retry |
| Plain 401 (bad token) |
Existing "This URL is invalid or expired." — unchanged |
Condition of satisfaction
Flow:
Tests/commands:
Related user story
#47116
Task
Teaches the My device page (DeviceUserPage) the SSO dance: when a device API call returns the SSO-required 401, initiate device SSO and navigate to the IdP; after the callback redirects back with the session cookie, the page loads normally. Handles expired sessions (mid-visit 401 → re-initiate) and SSO failure states. No Figma exists for this surface (spec open question 5) — states below are the proposed minimum; confirm with product before polishing.
Context for a fresh session: read
tmp/47116/spec.md(spec artifacts in the spec lead's local checkout; gitignored) ("Device SSO session and cookies" narrative). Contracts from sub-issues 2/3:POST /api/_version_/fleet/device/{token}/sso→{"url": "<IdP URL>"}+ handshake cookie; SSO-exempt, device-token-authenticated."sso_required": truein the JSON body when the setting is on and no valid session cookie accompanies the token. A plain 401 (no marker) still means invalid/expired token → existing error page./device/{token}; the session cookie is set server-side — the SPA never touches it.Detect SSO-required
frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx— the page already special-cases auth failures:isAuthenticationErrorderived from the device-details query error at:360-361, rendered viaDeviceUserError(frontend/components/DeviceUserError/DeviceUserError.tsx:48-64, "This URL is invalid or expired."). Bonus signal: during Setup Experience, orbit opens the page with?setup_only=1(orbit/pkg/setup_experience/setup_experience.go:187) — the server-side carve-out (sub-issue 3) makes SSO a non-issue there, but the SPA can use the param as defense in depth to avoid ever auto-initiating in that mode. Extend the error classification: parse the API error body forsso_required: true(check howservices/entities/device_user.tssurfaces error bodies — follow the existingisDupDetailsErrorhandling) and branch:sso_required→ start the SSO flow (below) instead of rendering the invalid-URL error.Apply the same classification to the other device queries the page issues (policies, software, certificates) so a mid-visit session expiry re-initiates rather than erroring — centralize in one helper rather than per-query copies.
Initiate and redirect
Add the route to
frontend/utilities/endpoints.ts(it is not there yet), then the service method infrontend/services/entities/device_user.ts. Note the existing methods there build URLs fromDEVICE_USER_DETAILSas a string prefix (device_user.ts:71-89, e.g.`${DEVICE_USER_DETAILS}/${token}/refetch`) — follow that pattern:Flow component/state on DeviceUserPage:
initiateDeviceSSO(deviceAuthToken), thenwindow.location.href = response.url— same pattern asfrontend/pages/MDMAppleSSOPage/MDMAppleSSOPage.tsx(calls initiate, navigates to the returned IdP URL).Return leg
Nothing to build: the server 303s back to
/device/{token}and the queries just succeed. Verify the router treats the return URL identically (device routes atfrontend/router/index.tsx:457-466, outside the authenticated/unauthenticated wrappers) and that any sub-route the user started on (/device/{token}/self-service,/software,/policies—frontend/router/paths.ts:187-199) survives the round-trip: the initiate endpoint'soriginalURLcomes from the server; pass the current full path if sub-issue 2 supports it, else landing on the root device page is acceptable v1.States summary (proposed minimum — no Figma)
Condition of satisfaction
Flow:
/device/{token}initiates SSO and navigates to the IdP URL; on return the page renders fully with no extra prompt./device/{token}/self-service) returns the user to a working page after SSO (root landing acceptable if originalURL doesn't carry sub-routes).Tests/commands:
yarn test— DeviceUserPage tests cover: sso_required classification vs plain 401, auto-initiate call + redirect, loop guard, terminal states.make lint-jsclean.tmp/47116/spec.mdTesting tips, steps 4–7).