Skip to content

fix(integrations): source OAuth initiator userId from auth context#2720

Merged
tofikwest merged 2 commits intomainfrom
fix/oauth-userid-from-auth-context
Apr 30, 2026
Merged

fix(integrations): source OAuth initiator userId from auth context#2720
tofikwest merged 2 commits intomainfrom
fix/oauth-userid-from-auth-context

Conversation

@tofikwest
Copy link
Copy Markdown
Contributor

Summary

  • Production regression fix. Every OAuth integration flow (GitHub, GCP, AWS, Rippling, Linear, Slack, etc.) is currently broken in prod, redirecting with error=session_mismatch&error_description=OAuth flow can only be completed by the user who initiated it.
  • The frontend hook posts userId: '' with a comment claiming the API will fill it in from the auth context, but oauth.controller.ts reads userId straight from the request body — so every IntegrationOAuthState row is written with an empty string.
  • This was harmless until fix(security): address pentest findings — admin escalation, secrets RBAC, oauth session check #2712 (yesterday) added a defense-in-depth session check that compares session.user.id to oauthState.userId. Empty string never matches a real user ID → every flow fails.
  • Resolve userId via the @UserId() decorator, drop the field from StartOAuthDto, and stop sending it from the client. The defense-in-depth check from fix(security): address pentest findings — admin escalation, secrets RBAC, oauth session check #2712 is preserved and now actually works as intended.

Why the spec didn't catch it

The new callback tests in #2712 mock oauthState directly with userId: 'user_1' rather than exercising startOAuth → callback end-to-end, so the empty-userId write path was never exposed.

Scope

Strictly in scope. Verified:

  • IntegrationOAuthState.userId is read in only two places (the new check at oauth.controller.ts:475 and a warning log at :289) — both will continue working with the real userId.
  • The repository (oauth-state.repository.ts) only queries by state (the random token), never by userId.
  • No usages in comp-private, apps/portal, or apps/framework-editor.
  • Only one caller of /v1/integrations/oauth/start: the frontend hook, which is updated here.

Deploy-order safety

  • API ships first: old frontends keep working — extra userId: '' in the body is ignored, decorator wins.
  • Frontend ships first: the old API would crash on missing body field — but in practice this monorepo deploys API + app together.
  • No data migration needed: in-flight OAuth states with empty userId expire after 10 minutes via deleteExpired().

Test plan

  • npx jest src/integration-platform/controllers/oauth.controller.spec.ts — 19/19 passing
  • npx turbo run typecheck --filter=@trycompai/api — no new errors introduced (177 pre-existing in unrelated files, identical count before/after)
  • npx turbo run typecheck --filter=@trycompai/app — no new errors introduced
  • Manual smoke test in staging: connect GitHub via OAuth as the same user who initiated the flow → expect success
  • Manual smoke test: confirm session check still rejects when initiating in one browser and completing in another (the security goal of fix(security): address pentest findings — admin escalation, secrets RBAC, oauth session check #2712)

🤖 Generated with Claude Code

The frontend hook posted `userId: ''` with a comment that the API would
fill it in from the auth context, but the controller was reading userId
straight from the request body, so every IntegrationOAuthState row was
written with an empty userId.

This was harmless until #2712 added a defense-in-depth session check
on the OAuth callback that compares `session.user.id` to
`oauthState.userId`. Because the stored value was always empty, every
OAuth flow (GitHub, GCP, AWS, Rippling, etc.) now redirects with
`error=session_mismatch` and "OAuth flow can only be completed by the
user who initiated it." The spec did not catch this because the
callback tests mocked `oauthState` directly with a non-empty userId
rather than exercising start -> callback end-to-end.

Resolve userId via the @userid() decorator instead of trusting the
request body, drop it from StartOAuthDto, and stop sending it from the
client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
comp-framework-editor Ready Ready Preview, Comment Apr 30, 2026 0:50am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
app Skipped Skipped Apr 30, 2026 0:50am
portal Skipped Skipped Apr 30, 2026 0:50am

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/integration-platform/controllers/oauth.controller.ts">

<violation number="1" location="apps/api/src/integration-platform/controllers/oauth.controller.ts:92">
P2: `@UserId()` on this hybrid-auth route introduces a 500 failure path for API-key callers. Handle non-session auth explicitly (or reject it with a proper HTTP auth exception) instead of relying on a decorator that throws a generic `Error`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/api/src/integration-platform/controllers/oauth.controller.ts
Address cubic-dev-ai review feedback. With HybridAuthGuard alone, an
API-key or service-token caller would reach the @userid() decorator,
which throws a plain Error (turning into a generic 500) when no
session user is present.

Add SessionOnlyGuard between HybridAuthGuard and PermissionGuard so
non-session auth is rejected with a clean 403 and a clear message.
The OAuth callback already requires a real session (see
checkSessionMatchesState), so non-session callers could never have
completed the flow anyway — this just moves the rejection upfront.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel vercel Bot temporarily deployed to Preview – app April 30, 2026 12:49 Inactive
@vercel vercel Bot temporarily deployed to Preview – portal April 30, 2026 12:49 Inactive
@tofikwest
Copy link
Copy Markdown
Contributor Author

Addressed @cubic-dev-ai review feedback in 210a4f7e1.

The concern: The @UserId() decorator throws a plain Error for API-key callers, which NestJS turns into a generic 500 — bad UX and poor monitoring signal.

Fix: Added SessionOnlyGuard between HybridAuthGuard and PermissionGuard on startOAuth. Non-session auth is now rejected upfront with a clean 403 Forbidden and a clear message, before @UserId() is ever evaluated.

@UseGuards(HybridAuthGuard, SessionOnlyGuard, PermissionGuard)

Note: this is purely a UX/error-reporting improvement — non-session callers could never complete the OAuth flow anyway because the callback requires a real session via checkSessionMatchesState. This just rejects them with a proper HTTP exception at the start endpoint instead of a generic 500.

Tests still passing 19/19.

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented Apr 30, 2026

Addressed @cubic-dev-ai review feedback in 210a4f7e1.

The concern: The @UserId() decorator throws a plain Error for API-key callers, which NestJS turns into a generic 500 — bad UX and poor monitoring signal.

Fix: Added SessionOnlyGuard between HybridAuthGuard and PermissionGuard on startOAuth. Non-session auth is now rejected upfront with a clean 403 Forbidden and a clear message, before @UserId() is ever evaluated.
...

@tofikwest This is a great fix. Adding SessionOnlyGuard correctly preempts the @UserId() decorator, which would otherwise throw a generic Error and trigger a 500 when called via API key or service token.

By placing it after HybridAuthGuard, you've ensured that non-session requests are rejected with a clean 403 before the controller logic or decorators are even reached. The updated tests in oauth.controller.spec.ts also correctly reflect the new guard stack.

This effectively resolves the UX and monitoring concern I raised.

For more details on how guards and decorators interact in this codebase, you can check the API authentication docs.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 2 files (changes from recent commits).

Requires human review: This PR modifies core authentication logic and authorization guards for OAuth flows, which falls under security-sensitive changes requiring human review.

@tofikwest tofikwest merged commit 8ab1296 into main Apr 30, 2026
11 checks passed
@tofikwest tofikwest deleted the fix/oauth-userid-from-auth-context branch April 30, 2026 12:51
@claudfuen
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.39.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants