Skip to content

Add CLI OAuth device login#1590

Open
ChiragAgg5k wants to merge 24 commits into
mainfrom
codex/cli-oauth-device-login
Open

Add CLI OAuth device login#1590
ChiragAgg5k wants to merge 24 commits into
mainfrom
codex/cli-oauth-device-login

Conversation

@ChiragAgg5k

@ChiragAgg5k ChiragAgg5k commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds the new browser-based OAuth2 device login flow to the Appwrite CLI while preserving existing self-hosted email/password login and legacy cookie sessions. It also hardens session switching, logout, reset, token refresh, deployment log streaming, and local Cloud development behavior around the new auth model.

What Changed

Browser-based CLI login

  • Adds OAuth2 device authorization login for Appwrite Cloud endpoints.
  • Stores OAuth access token, refresh token, token expiry, and OAuth client ID in CLI preferences.
  • Refreshes expired access tokens through the OAuth refresh-token grant before authenticated API calls.
  • Verifies the newly-created OAuth session by calling /account before reporting login success.
  • Restores the previous session and removes partial login state if OAuth login verification fails.
  • Handles OAuth device polling errors more defensively, including missing/empty error response bodies.
  • Honors OAuth slow_down responses by increasing the polling interval.
  • Keeps login --new from showing the legacy-session warning for the command that is explicitly migrating to the new flow.

Backward compatibility and self-hosted login

  • Keeps the existing email/password login flow for self-hosted endpoints.
  • Preserves MFA support for self-hosted email/password login.
  • Fixes failed MFA cleanup so a bad OTP, cancelled prompt, invalid factor, or challenge failure does not leave a partial current session behind.
  • Allows stale/broken current sessions to recover during login instead of blocking the user from signing in again.
  • Adds a local development override so APPWRITE_CLI_DEV_CLOUD_LOGIN=1 treats localhost/loopback endpoints as Cloud for OAuth device login testing.

Session switching and auth precedence

  • Fixes login --switch so selecting an existing account actually switches instead of starting a new OAuth login.
  • Verifies the selected session before reporting switch success.
  • Restores the previous current session when a selected session is expired, revoked, or fails token refresh.
  • Updates project client auth precedence so OAuth bearer/cookie admin sessions are used before falling back to a saved API key.
  • Keeps regional project API endpoints intact while using the generic Cloud console endpoint only where the OAuth console flow requires it.

Logout, reset, and server-side cleanup

  • Revokes OAuth refresh tokens on logout/reset where possible.
  • Deletes legacy cookie sessions on the server instead of only removing local config.
  • During OAuth migration, revokes/removes legacy cookie sessions only after server cleanup succeeds, and keeps/warns for sessions that could not be revoked.
  • Fixes grouped-account logout where one saved session succeeds and a sibling fails: the CLI now restores a surviving failed session instead of leaving no current session.
  • Treats endpoint/key-only sessions as local-only entries so client --reset can fully clear local CLI configuration without requiring server revocation.

Deployment and realtime behavior

  • Allows realtime deployment log streaming to authenticate with OAuth bearer tokens.
  • Refreshes OAuth access tokens before opening deployment realtime connections, matching REST client behavior.
  • Fixes local Cloud deployment page links: when running against localhost, the CLI fetches the project region in memory and formats console links like project-fra-<projectId> without writing region data into appwrite.config.json.
  • Keeps production Cloud link behavior unchanged by deriving regions from regional Cloud endpoint hostnames.

Generated CLI output

  • Updates CLI templates and regenerates the CLI example output after template changes.
  • Updates CLI lockfile templates through the lockfile update workflow.
  • Uses the preview @appwrite.io/console package that includes OAuth2 device authorization, token refresh, and revoke support.

Testing

Commands run during this PR:

  • ./scripts/update-lockfiles.sh cli
  • php example.php cli
  • composer lint-twig
  • composer refactor:check
  • vendor/bin/phpunit --testsuite Unit
  • npm install in examples/cli
  • npm run build:types in examples/cli
  • npm run build:runtime in examples/cli
  • npm run build in examples/cli
  • npm run mac-arm64 in examples/cli
  • npx eslint lib/commands/generic.ts lib/commands/init.ts lib/sdks.ts lib/types.ts lib/commands/utils/deployment.ts in examples/cli

Manual/local verification performed:

  • Smoke-tested ./build/appwrite-cli-darwin-arm64 login --new --endpoint "http://localhost/v1" against a local Cloud server with APPWRITE_CLI_DEV_CLOUD_LOGIN=1.
  • Verified login --new no longer shows the legacy cookie-session warning for that command.
  • Verified OAuth-authenticated project calls against local Cloud after the matching Cloud auth bridge fix.
  • Verified project list-policies --limit 1 succeeds with bearer auth against local Cloud.
  • Verified local deployment page URL formatting produces http://localhost/console/project-fra-... for regional local Cloud projects.

@ChiragAgg5k ChiragAgg5k marked this pull request as ready for review June 17, 2026 11:05
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

  • Adds browser-based OAuth device authorization login to the generated Appwrite CLI while preserving legacy email/password and cookie-session flows.
  • Updates session storage, switching, logout, reset, token refresh, and authenticated client creation around OAuth access and refresh tokens.
  • Extends deployment realtime authentication and local Cloud development handling for OAuth-backed CLI usage.
  • Regenerates CLI package templates, lockfiles, and e2e fixtures for the updated auth behavior.

Confidence Score: 5/5

No blocking issues were identified in the reviewed changes.

The changes are broad but focused on CLI authentication behavior, with tests and generated output updated alongside the implementation.

T-Rex T-Rex Logs

What T-Rex did

  • OAuth CLI authentication was run to capture before and after harness outputs.
  • Session lifecycle behavior for device-poll intervals was validated using before/after artifacts to compare explicit zero against default behavior.
  • Deployment realtime flow was recorded, showing baseline and updated authorization flow including a Bearer token after a refresh and unchanged configuration.
  • Generated CLI exploration and build attempts were observed, including PHP unavailability, new CLI templates, npm install success, and TypeScript build issues with missing modules, plus presence of a preview package API surface.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 CLI template tree cannot be typechecked directly because constants.js imports have no generated constants.ts source

    • Bug
      • Running npm install in templates/cli succeeds, but the subsequent CLI type build fails before validating the new OAuth login code because many template TypeScript files import ../constants.js or ./constants.js while the template directory only contains lib/constants.ts.twig. The failure includes the changed files lib/auth/login.ts, lib/auth/oauth.ts, and lib/commands/generic.ts, so the changed auth code is not consumable by the template package without the generator rendering step.
    • Cause
      • The committed templates/cli package has plain .ts sources that import a generated constants module, but the corresponding plain lib/constants.ts is absent; only lib/constants.ts.twig exists. In this environment the requested generation command could not run because PHP is unavailable, and direct template package typechecking fails on this missing module.
    • Fix
      • Ensure generated CLI validation runs against rendered output that includes lib/constants.ts, or make the template package self-typecheckable by providing a non-Twig constants shim/generated fixture for templates/cli builds. If direct template builds are expected in CI, commit or generate lib/constants.ts before tsc.

    T-Rex Ran code and verified through T-Rex

Reviews (18): Last reviewed commit: "(fix): allow explicit zero device-poll i..." | Re-trigger Greptile

Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/utils/deployment.ts
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/sdks.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/questions.ts
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts Outdated
Comment thread templates/cli/lib/sdks.ts Outdated
Comment thread templates/cli/lib/commands/generic.ts
Comment thread templates/cli/lib/commands/generic.ts Outdated
Extract the OAuth device-login logic that landed in commands/generic.ts
into a cohesive lib/auth/ layer, preserving all behavior:

- lib/auth/oauth.ts: OAuth2 client factory, device-token polling, token
  refresh, revoke, and id_token decoding (getValidAccessToken moved here
  from sdks.ts; the 3x-duplicated Oauth2 client construction is unified
  behind createOauth2).
- lib/auth/session.ts: typed session accessor, classification + current-
  session helpers, deleteServerSession, and a single logoutSessions() that
  replaces the three near-identical cleanup loops.
- lib/auth/login.ts: login orchestration and flows (loginCommand,
  loginWithEmailPassword, loginWithOAuthDevice, switchToAccount,
  completeMfaLogin, getCurrentAccount).
- utils.ts: endpoint classifiers placed next to isCloudHostname.
- generic.ts slimmed from ~1060 to ~330 lines (command definitions only).

Repointed sdks.ts/deployment.ts imports and registered the new auth
files in CLI.php getFiles(). Device-login output uses process.stdout.write
(matching the existing spinner idiom); dropped thin wrapper helpers.
Comment thread templates/cli/lib/auth/session.ts
The questionsListFactors choices loader built its console SDK with
requiresAuth: false. Since the OAuth login change made sdkForConsole
attach the session cookie only when requiresAuth is true, the MFA factor
list was fetched as a guest and failed with 401 — blocking self-hosted
email/password logins that reach MFA without --mfa from picking a factor.

Use an authenticated client so the partial MFA session cookie is sent.
Add a logic-layer auth section to the CLI e2e script that asserts the
pure/near-pure functions introduced by the OAuth device-login work:
endpoint classification (cloud/regional/localhost/dev-override), id_token
decoding, authorization-pending detection, device-token polling
(success/retry/error/timeout via a fake oauth2), cached access-token
reuse, session classifiers, planSessionLogout grouping, and
restoreCurrentSessionFallback. Driven by AUTH_LOGIC_RESPONSES in Base.php
and the three CLIBun test classes; no new runner or mock-server changes.
Comment thread templates/cli/lib/auth/login.ts Outdated
Add lib/flags.ts, a central registry of CLI feature flags read from env
vars via isFlagEnabled("<name>") so future flags are a one-line addition.
Route the OAuth device-login gate (APPWRITE_CLI_OAUTH_LOGIN) and the
localhost-as-Cloud dev override (APPWRITE_CLI_DEV_CLOUD_LOGIN) through it,
replacing the ad-hoc per-flag helpers in utils.ts.

OAuth device login stays off by default: Cloud endpoints use email/password
login (and the legacy-cookie migration nudges stay silent) until the flag is
enabled. Covered by the e2e auth-logic assertions.
pollForDeviceToken now honors slow_down by increasing the polling interval
by 5s per RFC 8628 (it previously retried at the original interval), and
treats an empty/unrecognized error body during polling as a transient
pending response instead of aborting the device flow with a blank
AppwriteException. Genuine terminal errors still propagate. Adds e2e
coverage for the slow_down backoff and empty-body retry paths.
The CLI generator renders lib/constants.ts from constants.ts.twig (the only
entry registered in CLI.php getFiles); the plain constants.ts was a stale
leftover from an earlier refactor, missing CONFIG_RESOURCE_KEYS,
HOMEBREW_FORMULA, UPDATE_CHECK_INTERVAL_MS, and TOP_LEVEL_RESOURCE_ARRAY_KEYS.
It is never read during generation, so the generated SDK was unaffected, but
building the template dir directly imported the stale file and failed.
Removing it leaves a single source of truth. Generated output is unchanged.
Comment thread templates/cli/lib/auth/oauth.ts
…elf-signed legacy revoke

- pollForDeviceToken now defaults to a 5s interval when the device
  authorization response omits one (RFC 8628 §3.5); previously an undefined
  interval produced NaN and busy-polled the token endpoint until expiry.
- getCurrentAccount/switchToAccount no longer persist the normalized console
  endpoint back into the session. sdkForConsole already normalizes when
  building the console client, so persisting it overwrote a regional Cloud
  endpoint and could route later project calls to the generic Cloud host.
- deleteServerSession builds the legacy client with the target session's own
  selfSigned setting (added to SessionData) instead of the current session's,
  so revoking a self-signed legacy session during OAuth migration no longer
  fails TLS verification and strand the session.

Adds an e2e assertion for the default polling interval.
Comment on lines +138 to +141
globalConfig.setCurrentSession(sessionId);
const serverDeleted = await deleteServerSession(sessionId);
if (serverDeleted) {
globalConfig.removeSession(sessionId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Deleted session stays current

logoutSessions sets the global current session to each session it processes, but the multi-account logout path does not restore the original current session afterward. When a user logs out a non-current account and that revocation succeeds, this removes the selected session while leaving currentSession pointing at the deleted id. Later commands can then report no active session even though the original current session still exists. Preserve the caller’s current session after this helper, or revoke using the target session data without changing global current state.

Artifacts

Repro: focused logoutSessions harness

  • Contains supporting evidence from the run (text/javascript; charset=utf-8).

Stack trace captured during the T-Rex run

  • Keeps the raw stack trace available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

The previous default-interval guard treated interval <= 0 as omitted, so a
deviceAuth with interval 0 (used to poll without delay) was forced to the 5s
default and consumed the whole expiry window before the next attempt, breaking
the retry/slow_down/empty-body poll paths in CI. Only non-finite (omitted)
intervals now fall back to 5s; an explicit 0 is honored. Strengthens the
default-interval test to assert the fallback delay actually applies.
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.

1 participant