Oauth login flow#1
Open
danlkv wants to merge 14 commits into
Open
Conversation
| const ISSUER = process.env.PUBLIC_URL || `http://localhost:${PORT}`; | ||
| const oidc = await buildProvider(ISSUER); | ||
| const validateHostToken = makeValidateHostToken(oidc); | ||
| app.use(cookieParser()); |
Add "Host registration via OAuth (CLI login)" section to auth.spec.md describing the PKCE flow, lifetime, rate limiting, and node-oidc-provider as the implementation. Add /oauth/* endpoints + /auth/success to the REST table in protocol.spec.md. Rewrite Install + Activation sections of main.spec.md to remove HOST_KEY interpolation and introduce `codette login`. Save implementation plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-IP HOST_KEY token mechanism with a full OAuth Authorization
Server using node-oidc-provider, mounted at /oauth.
- File-backed storage adapter for AccessToken, RefreshToken,
AuthorizationCode, Grant, Session, Interaction (atomic tmp+rename writes
under \$OAUTH_DATA_DIR; lazy GC on read).
- Provider config: codette-cli public client, PKCE S256 required, ES256
signing keypair persisted to disk, COOKIE_SECRET safety gate for
non-localhost issuers, canonical redirect_uri pinned to
\${PUBLIC_URL}/auth/success (URL-normalized to handle trailing slash).
- Custom consent page (views/consent.html) with single trial button, CSRF
cookie + form-value double-submit, per-IP sliding-window rate limit
(default 5 / 15 days, env-configurable). Atomic claimIfAllowed check +
revokeTrialClaim rollback on grant/interactionFinished failure.
- /auth/success intermediate page (views/success.html) extracts the CLI's
localhost port from OAuth state, fetches the callback with mode:'no-cors',
falls back to copy-paste UI on failure. Guards against missing code param.
- /host WS connection now validates ?token=<access_token> via
provider.AccessToken.find() (validateHostToken extracted to
oauth/host-auth.js with unit tests). Legacy HOST_KEY env handling,
TOKEN_FILE, loadTokens/saveTokens, getOrCreateTokenForIp, and the
HOST_KEY interpolation in /install.sh are removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add \`codette login\` PKCE-driven OAuth flow:
- Opens browser at /oauth/auth with PKCE challenge + base64url-encoded
state containing a free localhost port.
- Race between localhost listener for the callback and a paste prompt
(for remote codette installs where the browser cannot reach the CLI).
- Token exchange via /oauth/token; persists {server, refresh_token,
username, password} to ~/.config/codette/credentials.json (mode 0600).
Username and password are the chat-domain creds for the browser HMAC
flow; collected via interactive prompts with auto-generated defaults.
Replace the static HOST_TOKEN read from credentials.json.hostKey with
getAccessToken() at startup: exchange the persisted refresh_token for a
fresh access_token, persist the rotated refresh_token if it changed,
exit cleanly with "Run: codette login" if refresh fails. Honors
CODETTE_ACCESS_TOKEN env var as a test-env bypass.
SERVER_URL precedence updated to CLI > credentials.json > config.json >
env > default (matches main.spec.md config table). install.sh is slimmed
to deliver only the binary + config.json with server URL — no
credentials prompts, no HOST_KEY interpolation. Clean error message on
login failure (no stack dumps).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- tests/oauth-flow.js: headless OAuth Authorization Code + PKCE dance
helper. Drives the full 5-step flow with a manual cookie jar
(CSRF + session cookies). Returns {access_token, refresh_token, ...}.
Used by the harness to mint host tokens without spawning a browser, and
by the e2e spec for the token-exchange step.
- tests/e2e-oauth-login.spec.js: Playwright test that drives a real
browser through /oauth/auth → consent page → /auth/success → the
localhost callback fetch, then asserts the listener received the code
and /oauth/token returns both access and refresh tokens.
- tests/start-test-env.js: drop HOST_KEY env-var passing; add
OAUTH_DATA_DIR (per-run isolation) + COOKIE_SECRET + PUBLIC_URL +
SERVER_HOSTNAME to the server child; mint an access token via
mintAccessToken and pass it as CODETTE_ACCESS_TOKEN to the host.
Request scope=openid offline_access and prompt=consent explicitly in all
auth URLs, so oidc-provider's default issueRefreshToken check passes and
the trial flow always returns a refresh_token (no override needed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…DME, dev-script) - docker-compose.yml: swap HOST_KEY/HOST_TOKEN_TTL for COOKIE_SECRET, PUBLIC_URL, SERVER_HOSTNAME - init.sh: generate COOKIE_SECRET instead of HOST_KEY; prompt for PUBLIC_URL with localhost vs TLS default; mention `codette login` in bootstrap echo - README.md: remove HOST_KEY rows; add OAUTH_DATA_DIR, COOKIE_SECRET, PUBLIC_URL, SERVER_HOSTNAME, TRIAL_* rows; note host obtains credentials via `codette login` - run_dev.sh: remove HOST_KEY; add OAUTH_DATA_DIR/COOKIE_SECRET/PUBLIC_URL to server env; mint tokens via tests/oauth-flow.js and pass as CODETTE_ACCESS_TOKEN to each dev host - doc/auth.spec.md: fix steps 2/5/6 — redirect_uri is <server>/auth/success; local port travels in base64url state; auth code arrives at CLI via success page JS fetch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restyle the OAuth HTML pages to match the chat UI's design tokens (dark default + light via prefers-color-scheme, mono font, accent #cc5500, 520px-wide cards). - consent.html: rename trial button to "Try without registration for N days". Single brand mark + lede + button. - success.html: paste-fallback path shows a 3-step instruction with <kbd>-styled \`codette login\` / "Or paste the code here:" references so the user knows which prompt to look for. Copy button with green-flash feedback (debounced timer so a second click cleanly restarts the 1.5s window). Successful localhost-callback delivery shows a 3-second countdown then redirects to /. - error.html (new): shared error-card template used by every interaction.js failure path (expired/missing interaction session, CSRF mismatch, rate limit, grant.save / interactionFinished failure). Each path gets a specific title + actionable hint pointing at \`codette login\` recovery. SessionNotFound is detected and routed to the same friendly "session expired" message. - provider.js: explicit Grant + IdToken TTLs (silences oidc-provider's default-TTL NOTICE warnings on startup). - e2e test updated for the new selectors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
host/login.js: paste prompt now loops on empty input (accidental Enter no longer fails the flow with "no code received"). Cancel affordance baked into the prompt text — "Enter the code here (Ctrl+C to cancel):" — so a re-prompt needs no extra nag line. Drop chatty preamble / separator lines around the authorize URL. run_dev_login.sh: preserve OAuth state and credentials.json between runs by default (the persisted refresh_token stays valid; the script skips \`codette login\` entirely when credentials are present). Add --fresh flag to wipe both and force a new sign-in dance. Replace 17-line login-flow explainer banner with a single status line. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Audit flagged these as low-value (each tests one function in isolation and either restates a behavior covered by a broader test or makes a claim the assertions don't verify): - storage: "expired entries return undefined and are GC'd" — the GC side-effect was never observed - trial: "claimIfAllowed is atomic" — restated blocking, didn't verify atomicity - trial: "revokeTrialClaim removes the most recent claim" — only verified one slot freed - provider: "buildProvider returns an oidc-provider instance" — smoke test asserting library-level properties - provider: "non-localhost issuer without COOKIE_SECRET exits" — monkey-patched process.exit; couples to exit mechanism not behavior Remaining 12 unit tests cover the broader behaviors (round-trip, rate-limit threshold, redirect-uri policy, host-auth branches) plus the Playwright e2e exercises the full flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the username-squatting hole flagged in Task 5. CLI is authoritative
for the username (host operator picks at \`codette login\` prompt); server
records the {username → sub} binding and enforces it on every /host WS
connect.
Flow:
- CLI passes login_hint=<username> in the /oauth/auth URL.
- Consent page renders username read-only ("Sign in as <alice>").
- Server's /oauth/interaction/:uid/trial handler re-reads login_hint, calls
claimUsername(name, sub) atomically (file-backed mapping under
\$OAUTH_DATA_DIR/username-owners.json). On 'taken' returns the styled
error page and refunds the per-IP trial slot.
- findAccount populates preferred_username on the access token's claims.
- /host WS handler looks up the bound username by sub and rejects any
clientUsername that does not match. Token is bound to exactly one name.
Adds:
- server/src/oauth/usernames.js — isValidUsername (lowercase, leading
letter, 2–32 chars, [a-z0-9_-]), isUsernameClaimed, lookupUsernameBySub,
claimUsername (atomic, returns claimed/reclaimed/taken/invalid).
- /auth/username-available/:name pre-flight endpoint (advisory; race
resolved at claim time).
- Read-only username display on consent.html with accent styling.
The host's credentials.json is never overwritten by the server — CLI keeps
its own copy of the username from the prompt; server-side binding is a
parallel record used only to enforce the slot at WS connect time.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
\`codette login\` now: - Prompts for username, validates format locally, then calls GET /auth/username-available/:name and re-prompts on conflict. - Fails soft if the server returns non-JSON (older deployment without the endpoint): prints "(server does not support availability check — skipping)" and lets the consent-time check be the only authority. - Passes login_hint=<username> in the /oauth/auth URL so the server can bind it at consent time and display read-only on the consent page. Test harness updates: mintAccessToken accepts an optional username and returns it; start-test-env.js threads the test username through. run_dev.sh passes alice/bob as usernames for the dev fixtures. Playwright e2e generates a unique random name per run and asserts it appears on the consent page. Spec: auth.spec.md updated for the new login_hint + binding flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-flight Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Task 6 commit shuffled SERVER_URL precedence to put credentials.json above env vars in pursuit of "credentials.json records the canonical server" semantics. That broke the standard expectation that env vars override persisted config: setting CODETTE_SERVER_URL=ws://localhost:3000 to redirect a debug run had no effect when credentials.json had a saved server URL (e.g. a deployed instance) — silently routing the CLI to the wrong host and producing a confusing "skipping availability check, got HTML" message. Reorder to: CLI > env > credentials > config > default (now consistent with CLIENT_USERNAME and CLIENT_PASSWORD). Also: - Print "Server: <url>" at the start of \`codette login\` so the resolved URL is visible before any HTTP traffic. - Install SIGINT/SIGTERM handlers in the login subcommand block so Ctrl+C during the prompt exits cleanly (no "unsettled top-level await" warning). - doc/main.spec.md precedence statement updated to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reject the pending ask() promise on readline SIGINT (PromptAborted) and catch it in the dispatch try/catch. Settling the awaited promise avoids Node's "Detected unsettled top-level await" warning that previously fired when the user aborted the prompt. (Note: the warning persists in some non-TTY signal-injection test paths because readline's SIGINT event only fires for terminal-delivered Ctrl+C. In an interactive terminal — the actual use case — the handler runs and the await settles cleanly.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…server-only run_dev.sh learns a --server-only flag (or SERVER_ONLY=1 env): start only the server, no alice/bob host spawns, tail the server log. Used by run_dev_login.sh which now owns just the login UX dance — isolated \$HOME, the interactive codette login call, and the host spawn. Removes the duplicated env-var setup, cleanup trap, stale-pid kill, client build, and server-startup boilerplate from run_dev_login.sh (was 148 lines, now 76). Total dev-script LOC: 248 → 176. run_dev_login.sh also reuses an existing server on :3000 when one is already up — so you can run \`./run_dev.sh\` in one terminal and \`./run_dev_login.sh\` in another to drive the login flow without restarting the server. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
No description provided.