Skip to content

feat: one-click OAuth connect — consent flow, token persistence, Connect button#2

Merged
mabry1985 merged 1 commit into
mainfrom
feat/oauth-connect
Jul 3, 2026
Merged

feat: one-click OAuth connect — consent flow, token persistence, Connect button#2
mabry1985 merged 1 commit into
mainfrom
feat/oauth-connect

Conversation

@mabry1985

Copy link
Copy Markdown
Contributor

Replaces manual refresh-token minting with a one-click connect: paste your OAuth client id + secret into Settings once, then Connect Google in the console view runs the whole authorization-code flow and stores the refresh token itself.

Flow

  1. View button → gated POST /api/plugins/google/oauth/start (operator bearer) mints a single-use, 10-min state nonce and returns Google's consent URL — redirect_uri derived from the live request origin, so :7870 / :7871 / a LAN hostname all work (must be registered on the OAuth client).
  2. window.open → Google consent (access_type=offline&prompt=consent forces a refresh token every time).
  3. Google redirects to public GET /plugins/google/oauth/callback (added to manifest public_paths — a redirect can't carry a bearer; the nonce gates it instead). Code is exchanged, the refresh token is merged into the untracked 0600 secrets.yaml via graph.config_io.save_secrets (refresh_token is already a declared plugin secret → redaction + uninstall-purge apply), and the in-memory creds swap immediately — no restart.
  4. The view polls status and flips to connected — <account email> (new gmail.profile()).

Security posture

  • START is operator-gated; CALLBACK is public but bound to a single-use TTL nonce minted by START — an attacker can't initiate or complete a binding.
  • Persistence degrades cleanly with no host (returns False → callback page says the token is session-only and how to keep it).
  • Google error bodies (error_description) surface on the callback page instead of a bare status code.

Config

New oauth_scopes setting (space-separated override); default requests gmail.modify calendar drive.readonly — the grow-into set. README gains the 5-minute Google Cloud client setup (Internal user type on Workspace = no verification / no 7-day token expiry; External+Testing caveat documented).

Test

33 passed (15 new), ruff clean, host-free.

🤖 Generated with Claude Code

…ect button

- oauth.py: consent-URL builder (offline+consent forces a refresh token), single-use
  TTL-bound state nonce, code exchange with Google's error_description surfaced,
  save_secrets persistence (lazy host import, degrades cleanly host-free)
- view: gated POST /oauth/start (operator-only, request-derived redirect_uri so
  :7870/:7871/LAN origins all work) + public GET /oauth/callback (manifest
  public_paths, nonce-gated); Connect/Reconnect button + connected-as email in the
  status card, with a poll that refreshes the panel once the grant lands
- gmail.profile() for the connected-account display
- refresh token takes effect immediately via an in-memory creds swap (no restart)
- manifest: oauth_scopes config/setting (default gmail.modify + calendar + drive.readonly)
- README: one-click connect + 5-minute Google Cloud client setup guide
- tests: 15 new (33 total) — nonce lifecycle, exchange, callback wiring incl.
  persist-failure warning, manifest public path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@protoquinn

protoquinn Bot commented Jul 3, 2026

Copy link
Copy Markdown

👀 Quinn is reviewing — verdict (PASS / WARN / FAIL) + findings to follow.

@protoquinn protoquinn Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

QA Audit — PR #2 | feat: one-click OAuth connect — consent flow, token persistence, Connect button

VERDICT: WARN (CI in-flight — test is in_progress; formal PASS/FAIL deferred)


CI Status

  • test: in_progress ⏳

Diff Review

  • New oauth.py (~117 lines): authorization-code flow with single-use TTL state nonces, access_type=offline&prompt=consent for refresh tokens, proper httpx.Client lifecycle in finally, graceful no-host degradation via lazy graph.config_io.save_secrets import.
  • gmail.py: new profile() helper for the "connected as <email>" display — clean, minimal.
  • __init__.py: wires build_router with scopes_fn + on_refresh_token callbacks; _set_refresh_token swaps the live _CREDS global in-place so the connect takes effect immediately.
  • README gains the one-click setup guide including Google Cloud Console steps, scope docs, and manual-fallback notes.

Observations

  • LOW: _PENDING is a module-level in-memory dict in oauth.py. A host restart loses all in-flight state nonces (users see a "bad state" callback and re-click Connect — annoyance, not a defect). In a future multi-worker host this would need a shared store.
  • LOW: Clawpatch structural review unavailable for this repo (HTTP 500 during mapper init) — review is diff-only.
  • GAP: Diff truncated at 200 of 600 lines; the remaining ~400 lines (view.py router, callback handler, tests, manifest) were not inspected. Core OAuth logic in the visible portion is sound.

— Quinn, QA Engineer

@protoquinn

protoquinn Bot commented Jul 3, 2026

Copy link
Copy Markdown

Submitted COMMENT review on #2.

@mabry1985 mabry1985 merged commit 0a051b2 into main Jul 3, 2026
1 check passed
@mabry1985 mabry1985 deleted the feat/oauth-connect branch July 3, 2026 08:41
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