feat(api): one OAuth redirect URI for all providers via a unified callback - #464
Open
biztex wants to merge 1 commit into
Open
feat(api): one OAuth redirect URI for all providers via a unified callback#464biztex wants to merge 1 commit into
biztex wants to merge 1 commit into
Conversation
…lback Consolidate the per-provider OAuth callback into a single GET /v1/apps/callback, dispatching on the provider committed into the HMAC-signed state at /authorize. Connecting Gmail, Drive, Calendar, and the rest of the Google family against one OAuth app now needs one registered redirect URI instead of one per service. Rollout is opt-in per config row so existing users keep working: - Saving a project app config stamps `redirectStyle: "unified"` into its settings - the form displays the unified URI at that moment, and the save disconnects the provider's connections anyway, so no live flow straddles the switch. - /authorize sends the unified redirect URI (and scopes the state cookie to it) only for stamped rows. Unstamped rows, env-default credentials, and org-scoped configs keep the per-provider URI their OAuth app already has registered. - The per-provider callback route stays untouched for those flows; both routes share one handler so the token exchange always repeats the exact redirect URI its authorization request used. The unified route fails flat with a 400 when the state does not verify: without a trustworthy provider there is no per-provider error page to redirect to, and nothing an attacker-forged state can steer. Closes onecli#301
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.
I have read the CONTRIBUTING.md file.
YES
What kind of change does this PR introduce?
Feature — a developer-experience improvement to the OAuth connection flow. Closes #301.
What is the current behavior?
Every provider has its own OAuth callback (
/v1/apps/:provider/callback), so connecting Google services against one OAuth app means registering a separate redirect URI in the Google console for each ofgmail,google-drive,google-calendar, … (a dozen-plus), each addition subject to Google's propagation lag. Reported in #301, where consolidating to a single callback path via thestateparameter was named as the direction to go — with rollout safety for existing users as the open question.What is the new behavior?
One shared callback —
GET /v1/apps/callback— usable by every OAuth provider. The provider is recovered from the HMAC-signedstateminted at the authenticated/authorize(the state already carriedprovider; the new route verifies the signature before trusting it). Both callback routes share one handler, so the token exchange always repeats the exact redirect URI its authorization request used.Rollout is opt-in per config row, so existing users keep working. The invariant: a config uses the unified URI if and only if it was saved through a surface that displayed the unified URI.
redirectStyle: "unified"into the row'ssettings. The config form now displays the unified URI — and saving a config already disconnects the provider's connections (refresh tokens are bound to the OAuth client), so no live flow straddles the switch.parseConfigBodystrips unknown keys, so a client cannot inject or downgrade the stamp; the server owns it.Fail-flat on bad state: the unified route returns a plain 400 when the state is missing, unverifiable, or names an unknown provider — without a verified provider there is no per-provider error page to redirect to, and nothing an attacker-forged state can steer (it already failed the HMAC).
Fragment-callback providers (Trello-shaped) work through the unified path too: their redirects carry no query params, and the
oauth_statecookie — scoped at/authorizeto the exact callback path in use — supplies the state, same as on the per-provider route.Changes
packages/api/src/routes/apps.ts— unifiedGET /apps/callbackroute (registered beforeGET /:provider);/authorizepicks the redirect URI and state-cookie path from the resolved config's redirect style; callback handler extracted and shared by both routes.packages/api/src/services/app-config-service.ts—upsertAppConfigstampsredirectStyle: "unified"on project-scope saves (org scope deliberately unstamped until the org surfaces display the unified URI).packages/api/src/apps/resolve-credentials.ts— surfaces the stamp asResolvedAppCredentials.redirectStyle.apps/web/.../connections/_components/redirect-uri.tsx(+ two call sites) — project config forms display the unified URI; org-scoped forms keep the per-provider URI.Tests
apps-unified-callback.test.ts(8 tests):/authorizesends the unified URI + cookie path only for stamped configs and keeps the per-provider URI otherwise; the unified callback completes the flow and hands the exchange the unified redirect URI; the fragment bridge resolves from the state cookie alone; 400 on missing/forged/unknown-provider state; the legacy per-provider callback is pinned unchanged.app-config-service.test.ts(+4 tests): stamp applied on create and update, disconnect-before-upsert ordering, client-suppliedredirectStyleignored, org rows unstamped.packages/apisuite: 786 passed, 0 failed.turbo run lint check-typesgreen for api/web/db/ui; prettier clean.Additional context
OAUTH_UNIFIED_REDIRECT=1) could follow as a small separate PR.