fix(oauth): support Basic Auth token exchange for Notion - #711
Conversation
Notion's token endpoint requires HTTP Basic Auth and a JSON body, but oauth_exchange only sent form-urlencoded client_secret. Provider 401s were also passed through and misread as session expiry in /connect/oauth. Add tokenExchangeStyle (form | basic-json) with Notion host auto-detect, map provider failures to 502 with error details, and show the real provider error in the connect UI. Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
π WalkthroughWalkthroughOAuth token exchange now supports ChangesOAuth token exchange
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client as connect-oauth
participant Handler as account-secrets
participant Helpers as oauth-token-exchange
participant Provider as OAuth provider
Client->>Handler: oauth_exchange with tokenExchangeStyle
Handler->>Helpers: resolve and build token request
Helpers->>Provider: form or basic-json token request
Provider-->>Handler: tokens or provider error
Handler-->>Client: tokens or standardized failure
Client->>Client: redirect only for session-expired classification
Possibly related PRs
π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
π Preview deployed: https://kody-pr-711.kody-a99.workers.dev Worker: Mocks:
|
There was a problem hiding this comment.
π§Ή Nitpick comments (2)
packages/worker/client/routes/connect-oauth.tsx (1)
300-321: π― Functional Correctness | π΅ Trivial | π€ Low value
isConnectOauthConfigdoesn't validate the now-requiredtokenExchangeStyle.
ConnectOauthConfig.tokenExchangeStyleis required (Line 69), but this guard doesn't check it, so a config persisted by an older build (before this field existed) still passes and is used withtokenExchangeStyle: undefined. It's currently self-recovering βJSON.stringifydrops the undefined key and the server re-infers the style fromtokenUrlβ but validating it here keeps the type contract honest.β»οΈ Optional guard addition
typeof record.flow === 'string' && + (record.tokenExchangeStyle === 'form' || + record.tokenExchangeStyle === 'basic-json') && typeof record.scopeSeparator === 'string' &&π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/worker/client/routes/connect-oauth.tsx` around lines 300 - 321, Add validation for the required ConnectOauthConfig.tokenExchangeStyle field in isConnectOauthConfig, ensuring it is a string (or otherwise matches the declared TokenExchangeStyle type) before accepting the record as valid. Place the check alongside the existing scalar property validations so persisted legacy configs without this field are rejected.packages/worker/src/mcp/capabilities/integrations/integration-shared.ts (1)
8-9: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winDerive this from
packages/worker/src/app/oauth-token-exchange.ts'stokenExchangeStyles. Keeping one source for the literals avoids the schema and resolver drifting apart.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/worker/src/mcp/capabilities/integrations/integration-shared.ts` around lines 8 - 9, Derive tokenExchangeStyleValues from the tokenExchangeStyles definition in oauth-token-exchange.ts instead of duplicating the literal values, and update its type/export usage as needed so the schema and resolver share a single source of truth.
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/worker/client/routes/connect-oauth.tsx`:
- Around line 300-321: Add validation for the required
ConnectOauthConfig.tokenExchangeStyle field in isConnectOauthConfig, ensuring it
is a string (or otherwise matches the declared TokenExchangeStyle type) before
accepting the record as valid. Place the check alongside the existing scalar
property validations so persisted legacy configs without this field are
rejected.
In `@packages/worker/src/mcp/capabilities/integrations/integration-shared.ts`:
- Around line 8-9: Derive tokenExchangeStyleValues from the tokenExchangeStyles
definition in oauth-token-exchange.ts instead of duplicating the literal values,
and update its type/export usage as needed so the schema and resolver share a
single source of truth.
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a09bdb59-031f-4fab-853b-b0d7739de8ca
π Files selected for processing (8)
packages/worker/client/routes/connect-oauth.node.test.tspackages/worker/client/routes/connect-oauth.tsxpackages/worker/src/app/handlers/account-secrets.node.test.tspackages/worker/src/app/handlers/account-secrets.tspackages/worker/src/app/oauth-token-exchange.node.test.tspackages/worker/src/app/oauth-token-exchange.tspackages/worker/src/mcp/capabilities/integrations/integration-save.tspackages/worker/src/mcp/capabilities/integrations/integration-shared.ts
Summary
Notion OAuth via
/connect/oauthfailed after the user approved access: Kody exchanged the code with a form-urlencodedclient_secretbody (no Basic Auth), Notion returned HTTP 401, and the connect UI treated that 401 as a Kody session expiry ("Session expired.") instead of showing the provider error. Tokens were never saved.Fix
tokenExchangeStyle:form(default, existing confidential body-secret flows) andbasic-json(HTTP Basic + JSON body).basic-jsonforapi.notion.com; allow explicit override via request body / integration metadata.error/error_description/providerStatusinstead of passthrough 401.Unauthorized.) from provider failures and surfaceserror_description.Test plan
npm run typecheckSystem recap β extends existing primitives (medium risk)
Mode: recap Β· Base:
main@0e95e1a8Β· Head:620a996dClassification: extends β outbound OAuth token exchange and integration config gain a
tokenExchangeStylecontract; connect UI error handling for/account/secrets.jsonoauth_exchange changes.Primitives touched
app-ui/connect/oauthexchange + failure messagingsecretsoauth_exchangeBasic Auth/JSON styles; provider errors β 502valuestokenExchangeStyleon integration configSystem map
Connect OAuth token exchange now chooses form vs Notion-style Basic+JSON before calling the provider, and provider auth failures no longer look like Kody session expiry.
Legend: green = composes (wiring only) Β· amber = extended by this PR Β· red = new primitive Β· gray = context (unchanged, included only when an edge crosses it).
Change flow
Before / after
client_secreterror_descriptionInvariants
userId.Summary by CodeRabbit