Skip to content

fix(auth): stop the unwired browser-PKCE provider from stalling login - #86

Merged
abrichr merged 1 commit into
mainfrom
fix/browser-pkce-unavailable-until-wired
Jul 28, 2026
Merged

fix(auth): stop the unwired browser-PKCE provider from stalling login#86
abrichr merged 1 commit into
mainfrom
fix/browser-pkce-unavailable-until-wired

Conversation

@abrichr

@abrichr abrichr commented Jul 28, 2026

Copy link
Copy Markdown
Member

The bug

openadapt-desktop login on macOS or Windows opens a browser tab and then blocks for 180 seconds before falling back to token paste.

available_providers() puts BrowserPkceProvider first on every interactive desktop, and its is_available() returned True on macOS/Windows regardless of configuration. _exchange_code is the first step that needs a Supabase project, but it runs after the loopback wait — so the failure is discovered only once the full timeout (default 180.0) has elapsed.

Reproduced against origin/main on macOS with no configuration:

origin/main is_available() on this desktop: True
login() raised after 3.0s: Timed out waiting for the browser login to complete.   # timeout=3.0 for the demo; the default is 180.0
browser tabs opened: 1
URL opened: https://app.openadapt.ai/login?redirect_to=http%3A%2F%2F127.0.0.1%3A58201%2Fcallback&code_challenge=...

Why it can never succeed

The provider is not wired end to end, and every missing piece is hosted-side:

Step Blocker
Browser redirects to loopback GET /login reads only checkout_session_id and next. It ignores redirect_to, code_challenge, code_challenge_method, state.
Supabase accepts the redirect The uri_allow_list is rewritten on every production deploy and holds no loopback entry.
Mint an ingest token POST /api/ingest-tokens authenticates the browser session cookie and requires name. It answers 401 to this provider's Authorization: Bearer <supabase access token>, and the provider sends label, not name.

Verified live against app.openadapt.ai: the four query parameters land in the Next.js RSC searchParams payload and are never consumed by the page.

OPENADAPT_SUPABASE_URL / OPENADAPT_SUPABASE_ANON_KEY are referenced nowhere except this module — not in openadapt-cloud, openadapt-flow, openadapt-tray, or any CI configuration. The provider has never been able to complete anywhere.

The change

Make is_available() require the Supabase configuration up front. It is a necessary condition the provider already depends on, so it now reports unavailable instead of costing the operator a silent multi-minute stall, and PasteTokenProvider — which works — runs immediately.

Deliberately minimal:

  • Nothing is removed. The provider and the chain order are unchanged.
  • Reversible. Configuring the environment and the hosted contract restores the flow.
  • Client-side only. No hosted or deployment change; nothing here touches production.

Tests

test_full_flow previously passed only because it stubs both _exchange_code and _mint_ingest_token — precisely the two halves the server does not implement. It now runs against a configured provider, so it still covers the wired path.

New regression tests cover the unconfigured case, partial configuration (URL without key and key without URL), and the user-visible symptom — that no stray browser tab is opened.

478 passed in tests/test_engine/, ruff clean. The one unrelated failure locally is test_presentation_export needing openadapt_types, which is not installed in this environment and is untouched by this change.

Note on direction

The shipped path to a credential remains the one-time pairing flow in engine/auth/pairing.py (oap_ secret → /api/local-bridge/pairings/{claim,confirm,abort}), which the hosted control plane does implement. This PR does not propose reviving browser-PKCE; it stops an unwired provider from degrading the login path that works. Reviving it is a hosted-contract decision.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

`available_providers()` puts `BrowserPkceProvider` first on every interactive
desktop, and its `is_available()` returned True on macOS/Windows regardless of
configuration. `_exchange_code` is the first step that needs a Supabase project,
but it only runs *after* the loopback wait -- so `openadapt-desktop login` opened
a browser tab and then blocked for the full 180s default timeout before failing
over to `PasteTokenProvider`.

Reproduced against this commit's parent on macOS with no configuration:
`is_available()` -> True, one tab opened to
`https://app.openadapt.ai/login?redirect_to=http://127.0.0.1:<port>/callback&...`,
then `RuntimeError: Timed out waiting for the browser login to complete.`

The provider is not wired end to end, and the missing pieces are all hosted-side:

* `GET /login` reads only `checkout_session_id` and `next`. It ignores
  `redirect_to`, `code_challenge`, `code_challenge_method` and `state` -- verified
  live against app.openadapt.ai, where those four land in the RSC `searchParams`
  payload and are never consumed. The browser therefore never navigates to the
  loopback listener.
* The Supabase `uri_allow_list` is rewritten on every production deploy and holds
  no loopback entry, so a `127.0.0.1` redirect would be rejected anyway.
* `POST /api/ingest-tokens` authenticates the browser session cookie and requires
  a `name` field; it answers 401 to the `Authorization: Bearer <supabase access
  token>` this provider sends, and the provider sends `label` rather than `name`.

`OPENADAPT_SUPABASE_URL` / `OPENADAPT_SUPABASE_ANON_KEY` are referenced nowhere
except this module, so the provider has never been able to complete anywhere.

Make `is_available()` require the Supabase configuration up front. That is a
necessary condition it already depends on, so the provider now reports
unavailable instead of costing the operator a silent multi-minute stall, and the
paste provider -- which works -- runs immediately. Nothing is removed and the
chain order is unchanged: configuring the environment plus the hosted contract
restores the flow.

The existing `test_full_flow` passed only because it stubs both `_exchange_code`
and `_mint_ingest_token` -- exactly the two halves the server does not implement.
It now runs against a configured provider so it still covers the wired path, and
new regression tests cover the unconfigured case, partial configuration, and the
user-visible symptom (no stray browser tab).

The shipped path to a credential remains the one-time pairing flow in
`engine/auth/pairing.py`, which the hosted control plane does implement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
@abrichr
abrichr merged commit 5b8e6c4 into main Jul 28, 2026
15 checks passed
@abrichr
abrichr deleted the fix/browser-pkce-unavailable-until-wired branch July 28, 2026 03:04
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