feat(site): OAuth2 device, consent and public-client admin screens (early sketches) by designertyler · Pull Request #28390 · coder/coder · GitHub
Skip to content

feat(site): OAuth2 device, consent and public-client admin screens (early sketches) - #28390

Closed
designertyler wants to merge 5 commits into
mainfrom
oauth-device-flow-screens
Closed

feat(site): OAuth2 device, consent and public-client admin screens (early sketches)#28390
designertyler wants to merge 5 commits into
mainfrom
oauth-device-flow-screens

Conversation

@designertyler

@designertyler designertyler commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Early sketches of three OAuth2 surfaces, opened as a draft to get design feedback. None of them are wired to anything — they depend on backend work that isn't merged.

  • Flow 1 — Device authorization grant (RFC 8628), route /device
  • Flow 2 — Consent / scope page (PLAT-470 / PLAT-479), route /oauth2/authorize
  • Flow 3 — Public-client admin UI (PLAT-504), deployment settings

Flows 1 and 2 are standalone pages with no dashboard chrome, built on SignInLayout + Welcome like /cli-auth and login/device. Flow 3 is admin-facing and sits inside dashboard chrome. All three follow the Coder Kit guidance. 41 Storybook stories cover every state.

Flow 1 — Device authorization

Step Component States
1. Enter code DeviceAuthEnterCodeView empty, prefilled, submitting, incomplete (field validation), not recognized, expired
2. Confirm DeviceAuthConfirmView default, single scope, long client name, submitting
3. Result DeviceAuthResultView approved, denied, expired

Step 1 is skipped when the user arrives with ?user_code= (the verification_uri_complete case). Step 2 shows the code back to the user because RFC 8628 §5.4 requires it. Step 3 has no redirect — the CLI polls and picks up the result itself, so each state ends by saying the tab is safe to close.

Flow 2 — Consent / scope page

Screen Component States
Consent OAuth2ConsentPageView default, read-only, destructive scopes, unrestricted, no scope requested, unrecognized scope, many scopes, long client name, submitting
Redirect OAuth2ConsentRedirectView approved, denied
Error OAuth2ConsentErrorView unknown scope, scope not allowed, no grantable scope, invalid redirect, unknown client

Builds on #28045, which adds Scopes []string to RenderOAuthAllowData and lists the raw strings. scopes.ts maps the catalog in coderd/rbac/scopes_catalog.go (externalLowLevel + externalComposite) to descriptions, groups them by resource and orders the most consequential first. Raw scope names stay visible in mono underneath — the description is what the decision is made on, the identifier is what gets debugged later. The three errors added in that PR (errUnknownScope, errScopeNotAllowed, errNoGrantableScope) each get a screen.

Decisions worth arguing about:

  1. all, or no scope at all, is a warning rather than a list item. feat: validate and persist OAuth2 authorization scope #28045 falls back to the existing "full access to your account" sentence. Listing a full grant as if it were one permission understates it, so it gets a prominent warning Alert — the one place prominent is used, which the Kit reserves for page-level conditions.
  2. Delete permissions carry a "Can delete" badge, so the signal isn't colour alone.
  3. Unrecognized scopes are shown and labelled, not hidden. Silently dropping a permission the user is about to grant is worse than admitting the page can't describe it.
  4. The redirect host is on the screen — the user is being sent somewhere with a token, and should see where.
  5. Long scope lists scroll inside the card. 13 scopes made the page 1210px and pushed Approve/Deny below the fold, which is how people approve without reading. Capped at 666px with the decision always visible.
  6. No "try again" on errors — the request is malformed at the source. Each error says who can fix it, since it is never the person reading the screen.

Flow 3 — Public-client admin UI (PLAT-504)

Screen Component States
Client list OAuth2ClientsPageView default, empty, loading, view-only
Add application OAuth2ClientFormView confidential, public, validation on submit, submitting
Client detail OAuth2ClientDetailView confidential, public, confidential with no secrets, secret just generated

Admin-facing, so these sit inside dashboard chrome rather than using SignInLayout. Sketched as new components rather than edits to DeploymentSettingsPage/OAuth2AppsSettingsPage, so the existing pages stay reviewable side by side.

Decisions worth arguing about:

  1. A public client's secret section is absent, not disabled or empty. A disabled field still says a secret exists somewhere; an empty one invites the admin to hunt for the generate button. Neither is true, so the section is replaced by what the client authenticates with instead — the absence has to read as an answer, not as something that failed to load.
  2. Client type is its own table column. The Kit's table guidance is explicit that a distinct attribute buried as a subtitle can't be sorted or filtered. Type decides whether a client has a secret at all, which makes it the second most useful column on the page.
  3. Type is a Badge, not a StatusIndicator. Public/confidential is what a client is, not what it's doing. Colour only aids scanning; the text carries the meaning.
  4. The choice is explained where it's made. Picking "Public" in the add form swaps the "we'll generate a secret" copy for the PKCE explanation inline, rather than letting the admin discover the missing field on the next screen.
  5. RadioGroup, not Select, for client type — two options that need comparing, which is the case the Kit says RadioGroup exists for.

Open questions for reviewers

  1. Neither flow has a backend. coderd/oauth2provider/tokens.go has // TODO: Client creds, device code, and metadata.go advertises only authorization_code and refresh_token. Existing device-flow code (GitDeviceAuth, LoginOAuthDevicePage) is Coder acting as a client to GitHub, the mirror image of Flow 1.
  2. Flow 2 needs /oauth2/authorize served by the SPA. That URL is currently rendered by the Go template site/static/oauth2allow.html via ShowAuthorizePage, so the registered route is unreachable until the backend serves the SPA there and exposes the app record — name, icon, scopes, redirect URI — to the frontend. Bigger decision than the visual design.
  3. Scope descriptions live in the frontend. A deployment that adds scopes gets "Unrecognized". The alternative is the backend returning descriptions alongside the catalog, which is probably the right long-term answer.
  4. Does Coder's device implementation support verification_uri_complete? It decides whether Flow 1 step 1 is ever skippable in practice.
  5. The two flows both list scopes and should share one component. Kept separate here so each can be reviewed on its own; easy to extract once the treatment is agreed.
  6. SignInLayout clips at 200% zoom. Out of scope here — it's shared with login, /cli-auth and login/device. At a 640x400 viewport the Confirm screen's main sits at top: -139px, so the top of the card is unreachable. Changing h-screen to min-h-screen with vertical padding fixes it (verified locally); wants its own PR.
Design system audit — changes made after reading the Coder Kit guidance

Flow 1 was first built against the components as they exist in site/src/components, then revised against the Kit docs. Twelve things changed, and Flow 2 was built to the same rules from the start:

# Guidance What was wrong Fix
1 Input: don't disable submit until the form is valid Continue was disabled until 8 characters Always enabled; validates on submit, error below the field via aria-describedby + aria-invalid
2 Alert: subtle is the default, prominent is for page-level messages Both error alerts were prominent Subtle
3 Content style: sentence case everywhere PERMISSIONS REQUESTED, uppercase on the input "Permissions requested"; input uppercases values, not styling
4 Typography: don't set size, weight or line height per instance font-semibold, tracking-[0.2em], text-xs font-medium uppercase tracking-wide Scale tokens only
5 Icons: use size-icon-*, hide decorative icons size-8 on result icons size-icon-lg, aria-hidden
6 Spacing: prefer gap over margins mt-4/mt-6 stacks Flex columns with gap
7 Error page: no blame-the-user copy "Access denied", "Check for typos" "Device not connected"; "This code isn't recognized…"
8 Error page: always offer a way back Denied and expired dead-ended Every terminal state has one in-app destination
9 Button: change the label to say what's happening Spinner with static label "Checking…" / "Connecting…" / "Authorizing…"
10 Accessibility: every page needs a <main> landmark Missing Added to every screen
11 Content style: truncate don't wrap, pair with a tooltip Client name could wrap Truncated + tooltip
12 Accessibility: nothing clips at 200% zoom A long client name in the <h1> grew Confirm to 758px and pushed the top off-screen Headings are fixed; the requester is named in the card. Stable height at any name length

A follow-up fix after review: the device code field used WDJB-MJHT as its placeholder, which read as an already-filled field. It's now an XXXX-XXXX mask with the example moved to helper text.

Deliberate deviation: pages/error-page prescribes a full-page EmptyState for terminal feedback. The result and error screens use SignInLayout + Welcome instead, so every step of a flow reads as one surface, while adopting the error-page rules (factual copy, always a way back). Happy to switch if reviewers disagree.

Checks

pnpm check and pnpm lint:types pass.


🤖 Opened by Coder Agents on behalf of @designertyler.

@untra

untra commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

@designertyler designertyler changed the title feat(site): OAuth2 device authorization flow screens (early sketches) feat(site): OAuth2 device flow and consent screens (early sketches) Aug 20, 2026
@designertyler designertyler changed the title feat(site): OAuth2 device flow and consent screens (early sketches) feat(site): OAuth2 device, consent and public-client admin screens (early sketches) Aug 20, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants