Skip to content

feat: OAuth2 server consent & device verification screens#3087

Merged
ChiragAgg5k merged 4 commits into
mainfrom
feat-oauth2-consent-device
Jun 18, 2026
Merged

feat: OAuth2 server consent & device verification screens#3087
ChiragAgg5k merged 4 commits into
mainfrom
feat-oauth2-consent-device

Conversation

@ChiragAgg5k

Copy link
Copy Markdown
Member

What & why

Adds the "Sign in with Appwrite" identity-provider UI for the Console OAuth2 server — the two user-facing screens behind the new server env vars:

  • _APP_CONSOLE_OAUTH2_AUTHORIZATION_URL/oauth2/consent
  • _APP_CONSOLE_OAUTH2_VERIFICATION_URL/oauth2/device

When a third-party app (e.g. the Appwrite CLI) asks a user to authorize it against their Appwrite account, these are the screens they see.

Ported from appwrite/vibes#37, keeping the server-flow logic identical while adopting this repo's Pink design system (Svelte 5 runes, Card.Base, Pink Button, @appwrite.io/pink-icons-svelte, addNotification).

Screens

Consent (/oauth2/consent)

Authorization-code flow. Reads the grant, shows the requesting app's branding + what authorizing actually grants, and lets the user authorize or cancel. Approve redirects back to the client with a code; cancel returns access_denied.

The permission list leads with full account access rather than the requested OIDC scopes — see Accurate access framing below for why.

consent

Device — enter / confirm code (/oauth2/device)

Device Authorization Grant (RFC 8628). When opened via verification_uri_complete the code is rendered for the user to confirm it matches their device (no silent auto-submit); otherwise they type it in.

Confirm code (from ?user_code=) Enter code (manual)
device-confirm device-entry

Device — connected

After confirming the code the user sees the same consent card (with device-specific copy), then a terminal success state.

device-connected

Device — denied

device-denied

Accurate access framing

The consent screen previously listed only the requested OIDC scopes (Verify your identity / View your profile / View your email address), which implied read-only access. That is misleading.

This screen always authorizes against the console project. On the server, any OAuth2 access token issued for the console project is granted the full users (member) role — the same access a signed-in console session has — regardless of the requested scopes. The openid/profile/email scopes only shape the OIDC identity claims; they do not limit what the application can do.

So the consent screen leads with the full-access reality, followed by the identity scopes actually read (profile, email). openid is intentionally omitted — identity verification is implied by full account access, so listing it separately is redundant.

Logic fidelity (matches appwrite/vibes#37)

  • Consent: handles both the grant_id path (oauth2.getGrant + apps.get + account.get; 401 → login with redirect) and the raw client_id path (account.get → login if unauthenticated, else oauth2.authorize → redirect if redirectUrl / consent if grantId). Re-runs on param change with a local cancelled flag so a stale grant can never be approved against a different request.
  • Device: normalizeUserCode (upper, alnum only); always shows the code for confirmation — never auto-submits; oauth2.createGrant + apps.get → consent → terminal approved/denied states; oauth2_invalid_user_code → specific error message.
  • Approve/Reject: oauth2.approve/reject({grantId})window.location.href = redirectUrl (authorization flow) or terminal state (device flow).
  • Unauthenticated: both screens do their own account.get() check → redirect to /login carrying the current URL (routes live under (public), exempt from the root layout auth guard).

SDK

Bumps the @appwrite.io/console pin from 82d28311a5604f. The newer build adds the Oauth2 grant service (getGrant / authorize / approve / reject / createGrant) and the Models.Oauth2Grant / Oauth2Approve / Oauth2Reject types this feature relies on. Apps + Oauth2 are wired into createConsoleSdk so sdk.forConsole.oauth2.* and sdk.forConsole.apps.get are available at the same call-sites as the vibes PR.

Login redirect (MFA-safe)

The login page previously honored the redirect query param only for GitHub OAuth and the $redirectTo store — not for email login. An email user authorizing an OAuth2 app would land on the org console instead of resuming /oauth2/consent. Register already handled this correctly.

This PR makes email login honor redirect in an MFA-safe way: after createEmailPasswordSession, if a redirect param is present, it calls account.get(); on success it goto(redirect) + invalidate(ACCOUNT); on user_more_factors_required it falls through to invalidate(ACCOUNT) so the root layout routes to /mfa carrying the redirect param from the /login URL (preserving the OAuth2 resume target).

Design translation (shadcn/Tailwind → console Pink)

vibes PR (React + shadcn) this PR (SvelteKit + Pink)
Card Card.Base
Button variant="brandCta" / "outline" <Button submit> (primary) / <Button secondary>
lucide icons @appwrite.io/pink-icons-svelte (IconShieldCheck, IconCheck, IconExclamation, IconCheckCircle, IconXCircle, IconDesktopComputer)
toast addNotification
useMutation async handlers + $state
Input / Label native input (centered mono) + Label

New files use Svelte 5 runes ($props(), $state, $derived, $effect) per AGENTS.md.

Files

New

  • src/lib/helpers/oauth2-scopes.tsdescribeConsentScopes (leads with full access), describeScope generic fallback
  • src/routes/(public)/oauth2/+layout.svelte — centered .auth-bg shell + Appwrite logo footer
  • src/routes/(public)/oauth2/consent-card.svelte — shared consent card (scopes, approve/reject, errors, privacy/terms)
  • src/routes/(public)/oauth2/consent/+page.svelte — consent route
  • src/routes/(public)/oauth2/device/+page.svelte — device route

Modified

  • package.json / bun.lock — SDK pin bump
  • src/lib/stores/sdk.ts — wire Apps + Oauth2 into sdk.forConsole
  • src/routes/(public)/(guest)/login/+page.svelte — honor redirect after email login (MFA-safe)
  • src/lib/actions/analytics.tsSubmit.AccountOAuth2ConsentApprove / ...Deny / ...DeviceVerify

Verification

bun run format && bun run check && bun run lint && bun run tests && bun run build — all green.

  • bun run check: 0 errors (88 pre-existing warnings, none in new files)
  • bun run lint: 0 errors (345 pre-existing no-navigation-without-resolve warnings — same convention as every existing auth page)
  • bun run test:unit: 235 tests passed
  • bun run build: succeeds

Adds the 'Sign in with Appwrite' identity-provider UI for the Console
OAuth2 server — the two user-facing screens behind the new server env
vars:

- _APP_CONSOLE_OAUTH2_AUTHORIZATION_URL -> /oauth2/consent
- _APP_CONSOLE_OAUTH2_VERIFICATION_URL  -> /oauth2/device

Consent (/oauth2/consent): authorization-code flow. Reads the grant,
shows the requesting app's branding and what authorizing grants, and
lets the user authorize or cancel. Approve redirects back to the
client with a code; cancel returns access_denied. The permission list
leads with full account access rather than the requested OIDC scopes,
since console-project tokens receive the full users (member) role
regardless of scopes.

Device (/oauth2/device): Device Authorization Grant (RFC 8628). When
opened via verification_uri_complete the code is rendered for the user
to confirm it matches their device (no silent auto-submit); otherwise
they type it in. After confirming, the same consent card is shown
(with device-specific copy), then a terminal success state.

SDK: bump @appwrite.io/console pin to 1a5604f which adds the Oauth2
grant service and Models.Oauth2Grant; wire Apps + Oauth2 into
sdk.forConsole.

Login: honor the redirect query param after email login (MFA-safe) so
OAuth2 flows resume after sign-in, aligning login with register.
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds public Console OAuth2 consent and device verification routes.

Introduces a shared consent card with app branding, scope descriptions, approval, and denial handling.

Wires the Console SDK for Apps and OAuth2 grant APIs.

Updates email login redirect handling so OAuth2 resume flows can continue after sign-in.

Confidence Score: 5/5

No blocking issues were identified in the changed OAuth2 consent, device verification, SDK wiring, or login redirect handling.

The implementation is localized to the new public OAuth2 screens and related SDK/login plumbing, with the described validation commands reported as passing and no review comments remaining.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex attempted to run the Playwright harness for the consent flow and started Vite using bunx --bun vite, but the harness route requests did not complete.
  • The base and head commits were compared to verify the consent path was added in the head; the base lacked the new consent path and the head diff shows the OAuth2 consent route, layout, card, and scope helper.
  • A validation script was generated and a partial after-capture of the prefilled device-code route was recorded; the run remains inconclusive because base artifacts and the remaining scenarios were not captured.
  • OAuth2 login flow was exercised across multiple scenarios (SDK, login redirect, MFA, and non-MFA account.get error) using the oauth_login_sdk_harness.mjs harness.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (2): Last reviewed commit: "fix: pin ws to ^8.21.0 to clear high-sev..." | Re-trigger Greptile

Comment thread src/routes/(public)/oauth2/device/+page.svelte Outdated
Comment thread src/routes/(public)/oauth2/device/+page.svelte
Comment thread src/routes/(public)/oauth2/consent-card.svelte
Comment thread src/routes/(public)/(guest)/login/+page.svelte Outdated
Comment thread src/routes/(public)/oauth2/consent/+page.svelte Outdated
Comment thread src/routes/(public)/oauth2/device/+page.svelte
Comment thread src/routes/(public)/oauth2/consent/+page.svelte Outdated
The oauth2 layout wrapped its slot in the generic .console-container
class, which as a flex item in the row-flex section with no definite
width shrank to min-content, collapsing the card (and its width:100%
descendants) into a narrow sliver. Use a dedicated .oauth2-shell with a
fixed max-width and make the section stretch and center its content.
Addresses code review findings on the OAuth2 screens:

- consent: re-run the load effect on ANY authorize param change (full query
  string), not just grant_id/client_id, so a same-client request with a new
  redirect_uri/state/scope can't approve the previous grant
- consent: validate max_age as a non-negative integer, omitting it instead of
  forwarding NaN to the SDK
- consent-card: pin approve/reject to the grant id captured at call time and
  drop the result (and errors) if the parent swaps in a different grant mid-flight
- device: track only the URL user_code and reset loaded grant/app/phase when it
  changes or is removed, so stale device requests can't be confirmed
- device: ignore createGrant results/errors when the active code changed while
  awaiting, and guard against duplicate concurrent submits
- login: resume to the stored redirect exactly instead of appending the login
  page's leftover query params (e.g. message=) to the OAuth2 route
bun audit (CI gate) flagged ws 8.19.0 (transitive via jsdom) for
GHSA-96hv-2xvq-fx4p. Add an overrides pin to 8.21.0, matching the
repo's existing security-pin convention.
@ChiragAgg5k ChiragAgg5k merged commit 57b6376 into main Jun 18, 2026
3 of 4 checks passed
@ChiragAgg5k ChiragAgg5k deleted the feat-oauth2-consent-device branch June 18, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants