feat: OAuth2 server consent & device verification screens#3087
Merged
Conversation
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.
Contributor
Greptile SummaryAdds 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/5No 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.
What T-Rex did
Reviews (2): Last reviewed commit: "fix: pin ws to ^8.21.0 to clear high-sev..." | Re-trigger Greptile |
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.
levivannoort
approved these changes
Jun 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/deviceWhen 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, PinkButton,@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 returnsaccess_denied.The permission list leads with full account access rather than the requested OIDC scopes — see Accurate access framing below for why.
Device — enter / confirm code (
/oauth2/device)Device Authorization Grant (RFC 8628). When opened via
verification_uri_completethe code is rendered for the user to confirm it matches their device (no silent auto-submit); otherwise they type it in.?user_code=)Device — connected
After confirming the code the user sees the same consent card (with device-specific copy), then a terminal success state.
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. Theopenid/profile/emailscopes 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).openidis intentionally omitted — identity verification is implied by full account access, so listing it separately is redundant.Logic fidelity (matches appwrite/vibes#37)
grant_idpath (oauth2.getGrant+apps.get+account.get; 401 → login with redirect) and the rawclient_idpath (account.get→ login if unauthenticated, elseoauth2.authorize→ redirect ifredirectUrl/ consent ifgrantId). Re-runs on param change with a localcancelledflag so a stale grant can never be approved against a different request.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.oauth2.approve/reject({grantId})→window.location.href = redirectUrl(authorization flow) or terminal state (device flow).account.get()check → redirect to/logincarrying the current URL (routes live under(public), exempt from the root layout auth guard).SDK
Bumps the
@appwrite.io/consolepin from82d2831→1a5604f. The newer build adds theOauth2grant service (getGrant/authorize/approve/reject/createGrant) and theModels.Oauth2Grant/Oauth2Approve/Oauth2Rejecttypes this feature relies on.Apps+Oauth2are wired intocreateConsoleSdksosdk.forConsole.oauth2.*andsdk.forConsole.apps.getare available at the same call-sites as the vibes PR.Login redirect (MFA-safe)
The login page previously honored the
redirectquery param only for GitHub OAuth and the$redirectTostore — 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
redirectin an MFA-safe way: aftercreateEmailPasswordSession, if aredirectparam is present, it callsaccount.get(); on success itgoto(redirect)+invalidate(ACCOUNT); onuser_more_factors_requiredit falls through toinvalidate(ACCOUNT)so the root layout routes to/mfacarrying theredirectparam from the/loginURL (preserving the OAuth2 resume target).Design translation (shadcn/Tailwind → console Pink)
CardCard.BaseButton variant="brandCta"/"outline"<Button submit>(primary) /<Button secondary>@appwrite.io/pink-icons-svelte(IconShieldCheck,IconCheck,IconExclamation,IconCheckCircle,IconXCircle,IconDesktopComputer)toastaddNotificationuseMutation$stateInput/LabelLabelNew files use Svelte 5 runes (
$props(),$state,$derived,$effect) per AGENTS.md.Files
New
src/lib/helpers/oauth2-scopes.ts—describeConsentScopes(leads with full access),describeScopegeneric fallbacksrc/routes/(public)/oauth2/+layout.svelte— centered.auth-bgshell + Appwrite logo footersrc/routes/(public)/oauth2/consent-card.svelte— shared consent card (scopes, approve/reject, errors, privacy/terms)src/routes/(public)/oauth2/consent/+page.svelte— consent routesrc/routes/(public)/oauth2/device/+page.svelte— device routeModified
package.json/bun.lock— SDK pin bumpsrc/lib/stores/sdk.ts— wireApps+Oauth2intosdk.forConsolesrc/routes/(public)/(guest)/login/+page.svelte— honorredirectafter email login (MFA-safe)src/lib/actions/analytics.ts—Submit.AccountOAuth2ConsentApprove/...Deny/...DeviceVerifyVerification
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-existingno-navigation-without-resolvewarnings — same convention as every existing auth page)bun run test:unit: 235 tests passedbun run build: succeeds