fix(auth): stop the unwired browser-PKCE provider from stalling login - #86
Merged
Merged
Conversation
`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
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.
The bug
openadapt-desktop loginon macOS or Windows opens a browser tab and then blocks for 180 seconds before falling back to token paste.available_providers()putsBrowserPkceProviderfirst on every interactive desktop, and itsis_available()returnedTrueon macOS/Windows regardless of configuration._exchange_codeis the first step that needs a Supabase project, but it runs after the loopback wait — so the failure is discovered only once the fulltimeout(default180.0) has elapsed.Reproduced against
origin/mainon macOS with no configuration:Why it can never succeed
The provider is not wired end to end, and every missing piece is hosted-side:
GET /loginreads onlycheckout_session_idandnext. It ignoresredirect_to,code_challenge,code_challenge_method,state.uri_allow_listis rewritten on every production deploy and holds no loopback entry.POST /api/ingest-tokensauthenticates the browser session cookie and requiresname. It answers 401 to this provider'sAuthorization: Bearer <supabase access token>, and the provider sendslabel, notname.Verified live against
app.openadapt.ai: the four query parameters land in the Next.js RSCsearchParamspayload and are never consumed by the page.OPENADAPT_SUPABASE_URL/OPENADAPT_SUPABASE_ANON_KEYare referenced nowhere except this module — not inopenadapt-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, andPasteTokenProvider— which works — runs immediately.Deliberately minimal:
Tests
test_full_flowpreviously passed only because it stubs both_exchange_codeand_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 passedintests/test_engine/, ruff clean. The one unrelated failure locally istest_presentation_exportneedingopenadapt_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