feat(sync): OAuth 2.0 device-code login for CLI and Docker users#347
Conversation
Adds spec for --pantry and --ignore-pantry flags on the shopping-list command, mirroring the existing --aisle handling.
Replaces the browser-popup login (which fails in Docker / behind reverse proxies) with the OAuth 2.0 Device Authorization Grant. Fixes issue #290. Covers cook.md endpoints, CookCLI web UI changes, and a new `cook login` / `cook logout` CLI.
Cross-repo plan covering cook.md endpoints (Phase 1, TDD with RSpec) and CookCLI client + CLI commands (Phase 2). Matches the design at docs/superpowers/specs/2026-05-16-device-code-auth-design.md.
Previously, if a user initiated /api/sync/login and then called /api/sync/logout before completing browser approval, the spawned polling task would continue and could write a session file for the new authorization after logout.
- Hold pending_device_flow lock across request_device_code to close the TOCTOU race where two concurrent /api/sync/login calls could both pass the is_some check and orphan one device-code polling task. - Drop unused device_code and interval fields from PendingDeviceFlow; they were captured separately into the spawn closure. - Recompute countdown remaining from a fixed deadline each tick so the displayed time stays accurate in throttled / backgrounded tabs. - Unify device_flow public API on DeviceFlowError (request_device_code no longer returns anyhow::Result).
|
Thanks for the review. Addressed in 844f8d7:
Not addressed:
|
The 'should display current configuration' Playwright test uses a broad 'pre, code, .config-display' locator and asserts the first match is visible. The hidden device-code <code> element matched first and broke the test. Switch to <span> with identical monospace styling.
Server now returns {access_token, token_type, expires_in} per RFC 6749.
Align the CLI deserializer before clients ship — renaming the field
after release would be a breaking change.
Code Review — feat(sync): OAuth 2.0 device-code loginOverviewThis PR replaces the old browser-popup + local TCP listener approach with RFC 8628 (Device Authorization Grant), enabling Docker and headless deployments to log in to CookCloud. The core device-flow logic lives in What looks good
Issues and suggestions1. Ctrl-C is silent during the "Press Enter" prompt (login.rs)The Ctrl-C signal handler ( 2. Unrelated planning docs should not be in this PRThe diff includes three new
These add ~3 400 lines to the diff and make it significantly harder to review the actual code. They belong either in their own branches or should be removed from the repo if they are only agentic scaffolding. 3.
|
- login.rs: register the Ctrl-C handler before the "Press Enter" prompt and select between stdin and cancellation. Ctrl-C at the prompt now exits cleanly with a "Cancelled." message instead of terminating abruptly mid-print. - Rename LoginResponse.expires_in -> expires_in_secs so /api/sync/login and /api/sync/status agree on the field name. Drop the nullish- coalesce and the renderPending wrapper in preferences.html.
|
Thanks for the second pass. Addressed in 90225c8:
Not addressed (with reasoning):
|
Code Review: OAuth 2.0 Device Authorization Grant (PR #347)Good overall implementation of RFC 8628. The design is clean — removing the local TCP listener was the right call for Docker/headless environments, and the Critical Bug1.
|
| # | Severity | Location | Issue |
|---|---|---|---|
| 1 | Critical | device_flow.rs:~35 |
access_token should be token — login never completes |
| 2 | High | handlers/sync.rs |
Mutex held across HTTP await — blocks all concurrent callers |
| 3 | Medium | device_flow.rs, login.rs |
No reqwest timeout — unbounded block on hung server |
| 4 | Medium | device_flow.rs |
Cancel not checked during HTTP calls, only during sleep |
| 5–8 | Low | Various | reqwest pooling, CSRF note, stdin_task leak, beforeunload cleanup |
Blocker for merge: #1. Bug #2 should also be fixed before merge. Issues #3 and #4 can be tracked as follow-ups if the team accepts the risk.
Summary
Fixes #290 — Docker users (and any headless deploy) could not log in to CookCloud because the legacy flow opened a browser and waited on a local TCP listener that Docker containers cannot reach.
This PR implements the client side of OAuth 2.0 Device Authorization Grant (RFC 8628) against the new cook.md endpoints (server-side PR: cook-md/cook-md#62).
Changes
cook login/cook logoutCLI commands (gated onsyncfeature).cook loginrequests a device code, shows the verification URI + user code, optionally opens the URL viaopen, polls for the token, and persists the session — Ctrl-C cancels cleanly.verification_uri_complete, and a Cancel button. Polling resumes if the user navigates away and returns./api/sync/login,/api/sync/cancel_login,/api/sync/logout, and an enriched/api/sync/statusthat surfaces the pending flow. The old browser-popup handlers, local TCP listener, and ad-hoc callback parsing are gone (~150 LOC removed).src/server/sync/device_flow.rs— new module implementing the RFC 8628 client (typed errors viathiserror, slow-down handling, expiry, cancellation viaCancellationToken).Design spec
docs/superpowers/specs/2026-05-15-device-code-auth-design.mdKnown follow-ups (out of scope for this PR)
POST /api/sync/{login,cancel_login,logout}(legacy flow had same shape; tracked separately)./oauth/device/token(currently treated asBadResponse).Test plan
cargo build --features sync— cleancargo build --no-default-features --features self-update— cleancargo fmt --check— cleancargo clippy --features sync -- -D warnings— cleancargo test --features sync— passes (help-output snapshot updated)cook server, click Login in preferences, complete approval in another tab, confirm session landscook loginagainst a local cook.md, complete approval, confirm~/.config/cook/session.jsonwrittencook loginthen Ctrl-C mid-poll → "Cancelled." and no session filecook serverinside Docker, ensure login flow no longer needs a local listener