Skip to content

feat(web): [EE] OAuth2 authorization server with MCP support#977

Merged
brendan-kellam merged 15 commits intomainfrom
bkellam/mcp-oauth-SOU-264
Mar 5, 2026
Merged

feat(web): [EE] OAuth2 authorization server with MCP support#977
brendan-kellam merged 15 commits intomainfrom
bkellam/mcp-oauth-SOU-264

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Mar 4, 2026

Summary

  • Implements OAuth2 authorization code flow with PKCE (RFC 6749/7636) as an enterprise-only feature, gated behind the oauth entitlement
  • Adds MCP HTTP transport with OAuth2 bearer token authentication at /api/mcp
  • Implements OAuth2 discovery endpoints: Authorization Server Metadata (RFC 8414) and Protected Resource Metadata (RFC 9728)
  • Adds dynamic client registration (RFC 7591) with logo_uri support
  • Moves all OAuth API routes under /api/ee/ to reflect enterprise gating
  • Adds a polished authorization consent screen with client icon, details table, and approve/deny actions
  • Handles custom protocol redirect URIs (e.g. cursor://, claude://) via client-side redirect at /oauth/complete

New endpoints

  • POST /api/ee/oauth/register — dynamic client registration
  • GET /api/ee/oauth/authorize — authorization consent page
  • POST /api/ee/oauth/token — token exchange (authorization code → access token)
  • POST /api/ee/oauth/revoke — token revocation
  • GET /.well-known/oauth-authorization-server — server metadata (RFC 8414)
  • GET /.well-known/oauth-protected-resource/api/mcp — resource metadata (RFC 9728)
  • POST /api/mcp — MCP Streamable HTTP transport (authenticated)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • [EE] Full OAuth 2.x support: client registration, authorization/consent UI, token issuance (with refresh rotation), token revocation, and discovery/protected-resource metadata.
    • In-app authorize and completion flows, consent screen, and client branding/icons.
    • MCP API now surfaces OAuth metadata to clients on authorization failures to aid discovery.
  • Documentation

    • CHANGELOG guidance: enterprise-only features should be prefixed with [EE].

@github-actions

This comment has been minimized.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1fcd213a-f43e-47b3-b068-1688cbb07156

📥 Commits

Reviewing files that changed from the base of the PR and between 76c68af and 7fed80a.

📒 Files selected for processing (3)
  • packages/web/src/ee/features/oauth/server.test.ts
  • packages/web/src/ee/features/oauth/server.ts
  • packages/web/src/withAuthV2.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/web/src/ee/features/oauth/server.test.ts
  • packages/web/src/ee/features/oauth/server.ts

Walkthrough

Adds a full OAuth 2.0 authorization server and integration: DB schema and migrations, token generation/rotation/revocation logic, discovery and protected-resource endpoints, client registration, authorization UI and flows, entitlement gating, MCP error response augmentation, tests, and supporting constants/crypto.

Changes

Cohort / File(s) Summary
Changelog & Guidance
CHANGELOG.md, CLAUDE.md
Adds CHANGELOG entry for OAuth and guidance to prefix Enterprise-only features with [EE].
DB Schema & Migrations
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/20260304*/migration.sql, packages/db/prisma/migrations/20260304230353_add_oauth_resource_parameter/migration.sql, packages/db/prisma/migrations/20260304233124_add_oauth_refresh_tokens/migration.sql
Adds OAuth models (OAuthClient, OAuthAuthorizationCode, OAuthToken, OAuthRefreshToken), new columns (resource, logoUri), migrations to create tables, indices, and relations to User with cascade rules.
Shared Constants & Crypto
packages/shared/src/constants.ts, packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Introduces API/OAuth token prefixes (sbk_, sboa_, sbor_, legacy prefix) and new token generators (generateOAuthToken, generateOAuthRefreshToken); re-exports added.
Entitlements
packages/shared/src/entitlements.ts
Adds oauth entitlement and assigns it to self-hosted enterprise plans.
Auth Integration & Tests
packages/web/src/withAuthV2.ts, packages/web/src/withAuthV2.test.ts, packages/web/src/__mocks__/prisma.ts
Adds OAuth Bearer token auth path (entitlement check, token lookup, lastUsedAt update), supports API key prefixes (new + legacy), updates mocks and tests to cover OAuth flows and prefixes.
OAuth Server Logic & Tests
packages/web/src/ee/features/oauth/server.ts, packages/web/src/ee/features/oauth/server.test.ts
Implements core OAuth flows: generate/store auth codes (PKCE), verify+exchange codes (single-use), refresh token rotation with theft protection, revoke logic; comprehensive unit tests for success and failure cases.
OAuth Endpoints & Discovery
packages/web/src/app/api/(server)/ee/oauth/token/route.ts, packages/web/src/app/api/(server)/ee/oauth/register/route.ts, packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts, packages/web/src/app/api/(server)/ee/.well-known/oauth-authorization-server/route.ts, packages/web/src/app/api/(server)/ee/.well-known/oauth-protected-resource/route.ts, packages/web/src/app/api/(server)/ee/.well-known/oauth-protected-resource/[...path]/route.ts, packages/web/next.config.mjs
Adds token, registration, and revocation endpoints with entitlement checks and validation; serves RFC 8414 and RFC 9728 discovery/protected-resource metadata; adds rewrites for canonical .well-known paths.
Authorize UI & Actions
packages/web/src/app/oauth/authorize/page.tsx, packages/web/src/app/oauth/authorize/components/clientIcon.tsx, packages/web/src/app/oauth/authorize/components/consentScreen.tsx, packages/web/src/app/oauth/complete/page.tsx, packages/web/src/ee/features/oauth/actions.ts, packages/web/src/lib/posthogEvents.ts
Adds authorize page, consent UI and client icon, complete redirect page, server actions (approve/deny) resolving callback URLs, and PostHog event mappings for consent events.
MCP Integration
packages/web/src/app/api/(server)/mcp/route.ts
Wraps MCP service errors to inject WWW-Authenticate header with resource_metadata_uri on 401 responses to support RFC 9728 discovery.
Removed Middleware
packages/web/src/proxy.ts
Removes single-tenant proxy middleware and its matcher/configuration.
Tests & Mocks
packages/web/src/ee/features/oauth/server.test.ts, packages/web/src/withAuthV2.test.ts, packages/web/src/__mocks__/prisma.ts
Adds extensive OAuth server tests and updates mocks to include OAuth token/refresh token fixtures and new prefixes.

Sequence Diagrams

sequenceDiagram
    participant User as End User
    participant Browser as Browser
    participant Client as OAuth Client (MCP)
    participant AuthServer as Sourcebot Auth UI/Server
    participant DB as Database
    participant TokenEP as /oauth/token

    Browser->>AuthServer: GET /oauth/authorize (client_id, redirect_uri, code_challenge)
    AuthServer->>DB: Validate client_id and redirect_uri
    AuthServer->>Browser: Render consent (requires session)
    User->>AuthServer: Approve
    AuthServer->>DB: Store hashed authorization code
    AuthServer->>Browser: Redirect back to Client with code
    Client->>TokenEP: POST /oauth/token (grant_type=authorization_code, code, code_verifier, redirect_uri)
    TokenEP->>DB: Verify & delete code, create access + refresh tokens (transaction)
    TokenEP->>Client: Return access_token + refresh_token
Loading
sequenceDiagram
    participant Client as OAuth Client
    participant TokenEP as /oauth/token
    participant DB as Database
    participant MCP as MCP Server

    Client->>TokenEP: POST refresh_token grant (refresh_token)
    TokenEP->>DB: Lookup refresh token by hash
    TokenEP->>TokenEP: Validate clientId, resource, expiry
    alt valid
        TokenEP->>DB: Delete old refresh token (rotate)
        TokenEP->>DB: Create new access + refresh tokens
        TokenEP->>Client: Return new tokens
        Client->>MCP: Call MCP with access_token
        MCP->>DB: Validate access token
    else invalid
        TokenEP->>DB: Optionally revoke tokens (theft mitigation)
        TokenEP->>Client: Return error (invalid_grant)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

sourcebot-team

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(web): [EE] OAuth2 authorization server with MCP support' accurately and concisely describes the main change—implementing an OAuth2 authorization server with MCP support as an enterprise feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bkellam/mcp-oauth-SOU-264

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@brendan-kellam brendan-kellam changed the title feat(web): OAuth2 authorization server with MCP support (enterprise) feat(web): [EE] OAuth2 authorization server with MCP support Mar 4, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

♻️ Duplicate comments (1)
packages/web/src/app/oauth/complete/page.tsx (1)

10-13: ⚠️ Potential issue | 🔴 Critical

Block untrusted redirect targets and dangerous URL schemes.

On Line 12, the redirect uses a user-controlled query param directly. This enables open redirect and can allow javascript:-scheme execution/XSS. Validate protocol/target against a strict allowlist (and ideally a server-issued signed value) before navigation.

Suggested patch
 useEffect(() => {
-    const url = new URLSearchParams(window.location.search).get('url');
-    if (url) {
-        window.location.href = decodeURIComponent(url);
-    }
+    const raw = new URLSearchParams(window.location.search).get('url');
+    if (!raw) return;
+
+    try {
+        const target = new URL(raw);
+        const allowedProtocols = new Set(['https:', 'http:', 'cursor:', 'claude:']);
+        if (!allowedProtocols.has(target.protocol)) return;
+        window.location.assign(target.toString());
+    } catch {
+        // ignore invalid URL
+    }
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/complete/page.tsx` around lines 10 - 13, The
current redirect reads a user-controlled param via URLSearchParams,
decodeURIComponent and assigns it directly to window.location.href, which
enables open-redirects and dangerous schemes; replace this with strict
validation: parse the value with the URL constructor, reject non-http/https
schemes (no javascript:, data:, vbscript:), and enforce an allowlist for hosts
(or require same-origin) before setting window.location.href; alternatively
accept a server-signed token instead of a raw URL; update the code paths
referencing URLSearchParams, decodeURIComponent and window.location.href to
perform these checks and fall back to a safe internal route if validation fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 12: Update the CHANGELOG entry that currently reads "Oauth 2.1" to use
the correct canonical capitalization "OAuth 2.1" (the line containing "- [EE]
Added support for Oauth 2.1 to the remote MCP server hosted at `/api/mcp`.
[`#977`]"); replace "Oauth" with "OAuth" so the protocol name is consistently
capitalized.

In `@packages/db/prisma/schema.prisma`:
- Around line 496-497: Update the stale documentation comment above the Prisma
model OAuthClient to reference the correct endpoint path: change the comment
text that currently says "POST /api/oauth/register" to "POST
/api/ee/oauth/register" so the model comment matches the exposed endpoint;
locate the comment directly above the model OAuthClient declaration and adjust
only the path text.

In
`@packages/web/src/app/api/`(server)/ee/.well-known/oauth-protected-resource/[...path]/route.ts:
- Around line 7-8: The GET handler in route.ts currently skips entitlement
gating; add an entitlement check at the top of the exported GET function to
enforce the "oauth" entitlement and return a 403 response when the caller lacks
it. Locate the GET function (the route handler for [...path]) and call the
existing entitlement-check helper (or the same mechanism used in /register,
/token, /revoke) to verify the "oauth" entitlement before any metadata logic
runs; if the check fails, short-circuit and return a 403 Forbidden response so
tests and enterprise gating are satisfied.

In `@packages/web/src/app/api/`(server)/ee/oauth/register/route.ts:
- Around line 10-14: The registerRequestSchema is too permissive: change
redirect_uris and logo_uri validations to explicitly allow only safe schemes.
Replace redirect_uris: z.array(z.string().url()).min(1) with a validator that
parses each value as a URL and only accepts protocol "https:" (and optionally
"http:" for localhost/127.0.0.1), e.g. use z.string().refine(...) on each array
element to check new URL(value).protocol is allowed; similarly change logo_uri
from z.string().nullish() to an optional URL validator that only allows "https:"
(or a narrowly-scoped data:image mime whitelist if inline images are required)
using z.string().url().nullish().refine(...) to enforce the scheme. Ensure error
messages mention the allowed schemes and reference redirect_uris and logo_uri in
the schema.

In `@packages/web/src/app/api/`(server)/ee/oauth/token/route.ts:
- Around line 17-44: Replace the manual formData.get + null checks and
.toString() coercions with a Zod schema that validates grant_type ===
"authorization_code" and required string fields code, client_id, redirect_uri,
code_verifier; use schema.safeParse on the object built from await
request.formData(), and if safeParse fails return
requestBodySchemaValidationError(parseResult.error). On success, pass the
validated string values directly into verifyAndExchangeCode (use the exact
symbol names used now: verifyAndExchangeCode and
requestBodySchemaValidationError) and remove the old grantType !==
'authorization_code' and missing-parameter checks and .toString() calls.

In `@packages/web/src/app/api/`(server)/mcp/route.ts:
- Around line 21-27: The 401 handler currently always sets the WWW-Authenticate
header (using response.headers.set and env.AUTH_URL with
StatusCodes.UNAUTHORIZED), which can expose an OAuth flow even when the
caller/plan is not entitled; wrap the header-setting block in a check for the
oauth entitlement (e.g., verify the current request/plan entitlements or feature
flag such as plan.entitlements.oauth or an existing enableOauth() helper) so
that the header is only emitted when OAuth is enabled/entitled for that caller;
keep the same header value using env.AUTH_URL if the entitlement check passes.

In `@packages/web/src/app/oauth/authorize/page.tsx`:
- Around line 63-96: handleAllow and handleDeny capture the render-time session;
re-authenticate inside each server action and wrap them with sew() and the
appropriate auth helper instead of using session!.user.id. Update handleAllow to
call withAuthV2 (or withOptionalAuthV2 if optional) inside the server action to
obtain a fresh user object and pass its id to generateAndStoreAuthCode, and wrap
the whole action body with sew() for error handling; similarly ensure handleDeny
uses the same auth wrapper and sew() before constructing the redirect URL and
calling redirect. Keep generateAndStoreAuthCode and redirect calls unchanged but
use the authenticated user id from the auth helper and surface errors via sew().

In `@packages/web/src/features/oauth/server.ts`:
- Around line 8-10: The access token TTL constants ACCESS_TOKEN_TTL_MS and
ACCESS_TOKEN_TTL_SECONDS are set to 1 year; change them to a much shorter secure
default (e.g., 1 hour) and make the value configurable via an environment
variable (e.g., process.env.ACCESS_TOKEN_TTL_MS) with a validated numeric
fallback to the short default, update any other duplicated constants (the
similar definitions around lines 98-103) to use the same config source, and
ensure token issuance/verification logic in the OAuth code reads the configured
TTL rather than the hardcoded constant.
- Around line 57-90: The current single-use deletion (await
prisma.oAuthAuthorizationCode.delete({ where: { codeHash } })) can throw P2025
under concurrent exchanges; instead perform a deleteMany and check the returned
count to enforce single-use atomically: replace the delete call with something
like const result = await prisma.oAuthAuthorizationCode.deleteMany({ where: {
codeHash } }); then if (result.count === 0) return { error: 'invalid_grant',
errorDescription: 'Authorization code has already been used.' }; update the code
around prisma.oAuthAuthorizationCode.findUnique / codeHash / codeChallenge
verification to use this deleteMany check before issuing tokens so concurrent
requests get a clean invalid_grant rather than an exception.

In `@packages/web/src/withAuthV2.ts`:
- Around line 124-141: In withAuthV2, the code accepts sourcebot-oauth-* tokens
without checking the oauth entitlement; modify the bearerToken handling so the
OAuth path runs only when the runtime/user has the oauth entitlement (e.g. guard
the block that calls hashSecret and __unsafePrisma.oAuthToken.findUnique with an
entitlement check like hasEntitlement('oauth') or a similar existing entitlement
flag), otherwise fall through to the API key handling (getVerifiedApiObject).
Ensure the entitlement check surrounds the entire OAuth lookup/update logic so
issued OAuth tokens cannot authenticate when the oauth entitlement is disabled.

---

Duplicate comments:
In `@packages/web/src/app/oauth/complete/page.tsx`:
- Around line 10-13: The current redirect reads a user-controlled param via
URLSearchParams, decodeURIComponent and assigns it directly to
window.location.href, which enables open-redirects and dangerous schemes;
replace this with strict validation: parse the value with the URL constructor,
reject non-http/https schemes (no javascript:, data:, vbscript:), and enforce an
allowlist for hosts (or require same-origin) before setting
window.location.href; alternatively accept a server-signed token instead of a
raw URL; update the code paths referencing URLSearchParams, decodeURIComponent
and window.location.href to perform these checks and fall back to a safe
internal route if validation fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0a47a7b5-6e65-42f2-b5aa-29cac879fdfc

📥 Commits

Reviewing files that changed from the base of the PR and between f7ba084 and fa561e9.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • CLAUDE.md
  • packages/db/prisma/migrations/20260304051539_add_oauth_tables/migration.sql
  • packages/db/prisma/migrations/20260304212808_add_oauth_client_logo_uri/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/shared/src/crypto.ts
  • packages/shared/src/entitlements.ts
  • packages/shared/src/index.server.ts
  • packages/web/next.config.mjs
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/app/api/(server)/ee/.well-known/oauth-authorization-server/route.ts
  • packages/web/src/app/api/(server)/ee/.well-known/oauth-protected-resource/[...path]/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/token/route.ts
  • packages/web/src/app/api/(server)/mcp/route.ts
  • packages/web/src/app/oauth/authorize/components/clientIcon.tsx
  • packages/web/src/app/oauth/authorize/page.tsx
  • packages/web/src/app/oauth/complete/page.tsx
  • packages/web/src/features/oauth/server.ts
  • packages/web/src/proxy.ts
  • packages/web/src/withAuthV2.test.ts
  • packages/web/src/withAuthV2.ts
💤 Files with no reviewable changes (1)
  • packages/web/src/proxy.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/web/src/app/oauth/authorize/page.tsx (1)

64-98: ⚠️ Potential issue | 🟠 Major

Re-authenticate inside server actions; don’t rely on captured session.

Line 68 uses render-time session!.user.id. handleAllow and handleDeny should authenticate at action execution and wrap action bodies with sew() + withAuthV2/withOptionalAuthV2.

As per coding guidelines, "Server actions should wrap logic with sew() for error handling and use withAuthV2 or withOptionalAuthV2 from @/withAuthV2."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/authorize/page.tsx` around lines 64 - 98, The
server actions handleAllow and handleDeny currently rely on the render-time
captured session (session!.user.id); update both to re-authenticate inside the
action by wrapping their bodies with sew() and the appropriate auth wrapper
(withAuthV2 or withOptionalAuthV2 from '@/withAuthV2') so authentication happens
at execution time; inside handleAllow call withAuthV2/withOptionalAuthV2 to
obtain the authenticated user id to pass into generateAndStoreAuthCode (instead
of session!.user.id), and wrap both action implementations with sew() to handle
errors before performing the redirect logic that builds callbackUrl.
packages/web/src/features/oauth/server.ts (1)

8-11: ⚠️ Potential issue | 🟠 Major

Access-token default lifetime is too permissive for bearer tokens.

Line 9 sets a 1-year TTL. Please switch to a short secure default and make it configurable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/oauth/server.ts` around lines 8 - 11, The
ACCESS_TOKEN_TTL_MS constant currently sets a 1-year default which is too long
for bearer tokens; change the default to a much shorter secure value (e.g., 1
hour) and make it configurable: read a numeric TTL (ms) from configuration or
environment (e.g., process.env.ACCESS_TOKEN_TTL_MS) with parsing, validation and
a safe fallback to the new short default, then compute ACCESS_TOKEN_TTL_SECONDS
from that value; update any uses of ACCESS_TOKEN_TTL_MS/ACCESS_TOKEN_TTL_SECONDS
to rely on the configurable value and ensure no hard-coded 1-year constant
remains (refer to AUTH_CODE_TTL_MS, ACCESS_TOKEN_TTL_MS and
ACCESS_TOKEN_TTL_SECONDS to locate the code).
🧹 Nitpick comments (1)
packages/db/prisma/schema.prisma (1)

510-535: Consider indexing expiresAt for token/code cleanup efficiency.

If you run expiry sweeps or reporting later, lack of an expiresAt index can force full-table scans.

🛠️ Suggested schema tweak
 model OAuthAuthorizationCode {
@@
   expiresAt     DateTime
   createdAt     DateTime    `@default`(now())
+
+  @@index([expiresAt])
 }
@@
 model OAuthToken {
@@
   expiresAt  DateTime
   createdAt  DateTime    `@default`(now())
   lastUsedAt DateTime?
+
+  @@index([expiresAt])
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/prisma/schema.prisma` around lines 510 - 535, Add an index on the
expiresAt column for both OAuthAuthorizationCode and OAuthToken to avoid
full-table scans during expiry sweeps; modify the Prisma models
(OAuthAuthorizationCode and OAuthToken) to declare an index for the expiresAt
field (e.g., add a field-level `@index` or a model-level @@index([expiresAt]) on
each model), then run prisma migrate to generate the migration so the DB will
have the index for efficient cleanup queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/app/oauth/authorize/page.tsx`:
- Around line 34-44: The validation currently permits a missing
code_challenge_method but downstream enforcement requires S256; update the early
checks in authorize/page.tsx so that if code_challenge_method is falsy or not
equal to 'S256' the handler returns an ErrorPage with a precise message;
specifically modify the existing checks around client_id, redirect_uri,
code_challenge, response_type and the code_challenge_method branch (referencing
variables client_id, redirect_uri, code_challenge, response_type,
code_challenge_method and the ErrorPage return) to reject when
code_challenge_method is absent or not 'S256', yielding a clear protocol error
at authorization time.

In `@packages/web/src/features/oauth/server.ts`:
- Around line 71-73: The expiry cleanup should use deleteMany to avoid P2025
when another concurrent request already removed the row: replace the
prisma.oAuthAuthorizationCode.delete({ where: { codeHash } }) call in the
authCode.expiresAt branch with prisma.oAuthAuthorizationCode.deleteMany({ where:
{ codeHash } }) so deletion becomes idempotent and won’t throw if the record is
gone, then continue returning the existing { error: 'invalid_grant',
errorDescription: 'Authorization code has expired.' } response.

In `@packages/web/src/withAuthV2.ts`:
- Around line 135-140: Wrap the __unsafePrisma.oAuthToken.update call inside a
try/catch and handle Prisma "P2025" (record not found) by treating it as an
unauthenticated path instead of rethrowing: when calling
__unsafePrisma.oAuthToken.update({ where: { hash }, data: { lastUsedAt: new
Date() } }) in the oauthToken branch, catch errors and if error.code === 'P2025'
(or instance of PrismaClientKnownRequestError with code 'P2025') simply proceed
as if the token is missing (do not return oauthToken.user or throw), otherwise
rethrow; apply the same pattern to the apiKey.update call in the apiKey branch
so token/key deletion races become unauthenticated rather than 500s.

---

Duplicate comments:
In `@packages/web/src/app/oauth/authorize/page.tsx`:
- Around line 64-98: The server actions handleAllow and handleDeny currently
rely on the render-time captured session (session!.user.id); update both to
re-authenticate inside the action by wrapping their bodies with sew() and the
appropriate auth wrapper (withAuthV2 or withOptionalAuthV2 from '@/withAuthV2')
so authentication happens at execution time; inside handleAllow call
withAuthV2/withOptionalAuthV2 to obtain the authenticated user id to pass into
generateAndStoreAuthCode (instead of session!.user.id), and wrap both action
implementations with sew() to handle errors before performing the redirect logic
that builds callbackUrl.

In `@packages/web/src/features/oauth/server.ts`:
- Around line 8-11: The ACCESS_TOKEN_TTL_MS constant currently sets a 1-year
default which is too long for bearer tokens; change the default to a much
shorter secure value (e.g., 1 hour) and make it configurable: read a numeric TTL
(ms) from configuration or environment (e.g., process.env.ACCESS_TOKEN_TTL_MS)
with parsing, validation and a safe fallback to the new short default, then
compute ACCESS_TOKEN_TTL_SECONDS from that value; update any uses of
ACCESS_TOKEN_TTL_MS/ACCESS_TOKEN_TTL_SECONDS to rely on the configurable value
and ensure no hard-coded 1-year constant remains (refer to AUTH_CODE_TTL_MS,
ACCESS_TOKEN_TTL_MS and ACCESS_TOKEN_TTL_SECONDS to locate the code).

---

Nitpick comments:
In `@packages/db/prisma/schema.prisma`:
- Around line 510-535: Add an index on the expiresAt column for both
OAuthAuthorizationCode and OAuthToken to avoid full-table scans during expiry
sweeps; modify the Prisma models (OAuthAuthorizationCode and OAuthToken) to
declare an index for the expiresAt field (e.g., add a field-level `@index` or a
model-level @@index([expiresAt]) on each model), then run prisma migrate to
generate the migration so the DB will have the index for efficient cleanup
queries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b3f8699-74ac-4497-9765-70aca47bafee

📥 Commits

Reviewing files that changed from the base of the PR and between fa561e9 and 633f0e0.

📒 Files selected for processing (9)
  • packages/db/prisma/migrations/20260304230353_add_oauth_resource_parameter/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/app/api/(server)/ee/.well-known/oauth-protected-resource/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/token/route.ts
  • packages/web/src/app/oauth/authorize/page.tsx
  • packages/web/src/features/oauth/server.ts
  • packages/web/src/withAuthV2.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/token/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (4)
packages/web/src/app/oauth/authorize/page.tsx (2)

64-98: ⚠️ Potential issue | 🟠 Major

Re-authenticate inside handleAllow/handleDeny instead of trusting captured session.

Line 68 uses render-time session!.user.id. These server actions should authenticate at execution time and be wrapped with the standard error/auth helpers.

As per coding guidelines, "Server actions should wrap logic with sew() for error handling and use withAuthV2 or withOptionalAuthV2 from @/withAuthV2."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/authorize/page.tsx` around lines 64 - 98,
handleAllow and handleDeny use the render-time session (session!.user.id) but
must authenticate at action execution; update both server actions to call the
appropriate auth wrapper (withAuthV2 or withOptionalAuthV2 from "@/withAuthV2")
inside the action to obtain the up-to-date user info and replace
session!.user.id with the authenticated user id, and wrap the action body with
sew() for error handling; ensure generateAndStoreAuthCode is called with the
userId from the auth result and preserve the existing redirect logic unchanged.

34-44: ⚠️ Potential issue | 🟡 Minor

Require code_challenge_method and enforce S256 at authorization time.

Line 34 still permits missing code_challenge_method, while Line 42 only rejects non-S256 when present. This should fail early with a clear protocol error.

Suggested patch
-    if (!client_id || !redirect_uri || !code_challenge || !response_type) {
+    if (!client_id || !redirect_uri || !code_challenge || !code_challenge_method || !response_type) {
         return <ErrorPage message="Missing required OAuth parameters." />;
     }
@@
-    if (code_challenge_method && code_challenge_method !== 'S256') {
+    if (code_challenge_method !== 'S256') {
         return <ErrorPage message={`Unsupported code_challenge_method: ${code_challenge_method}. Only "S256" is supported.`} />;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/authorize/page.tsx` around lines 34 - 44, The
authorization parameter validation currently allows a missing
code_challenge_method; update the validation block in authorize/page.tsx to
treat code_challenge_method as required and to reject anything other than
'S256'. Specifically, alongside the existing checks for client_id, redirect_uri,
code_challenge and response_type, add/modify the check for code_challenge_method
so that if it's missing or not equal to 'S256' you return an ErrorPage (use the
same ErrorPage component) with a clear protocol error message referencing
code_challenge_method and that only "S256" is supported.
packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts (1)

17-25: ⚠️ Potential issue | 🟠 Major

Validate revoke payload via Zod and return standardized JSON success response.

Please replace manual formData.get() parsing with safeParse, return serviceErrorResponse(requestBodySchemaValidationError(...)) on parse failures, and use Response.json(...) for the 200 path.

Suggested structure
+const revokeBodySchema = z.object({
+  token: z.string().min(1).optional(),
+});
+
 const formData = await request.formData();
-const token = formData.get('token');
+const parsed = revokeBodySchema.safeParse(Object.fromEntries(formData.entries()));
+if (!parsed.success) {
+  return serviceErrorResponse(requestBodySchemaValidationError(parsed.error));
+}
+const { token } = parsed.data;
@@
-return new Response(null, { status: 200 });
+return Response.json({}, { status: 200 });

As per coding guidelines, "Request body (POST/PUT/PATCH) should use Zod schemas with safeParse and return requestBodySchemaValidationError if validation fails ... and return Response.json() for successful responses."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/api/`(server)/ee/oauth/revoke/route.ts around lines 17 -
25, Replace manual formData parsing in the revoke route with Zod validation:
parse request.formData() into an object and run requestBodySchema.safeParse(...)
(reference requestBodySchema and requestBodySchemaValidationError). If safeParse
fails, return
serviceErrorResponse(requestBodySchemaValidationError(parseResult.error)); if it
succeeds, extract the token string from parseResult.data and call
revokeToken(token) (reference revokeToken) and return Response.json({ success:
true }) for the 200 path instead of new Response(null, ...). Ensure you
import/use serviceErrorResponse and requestBodySchemaValidationError where
needed.
packages/web/src/app/api/(server)/ee/oauth/token/route.ts (1)

17-101: ⚠️ Potential issue | 🟠 Major

Switch token request parsing to Zod safeParse (both grant branches).

Manual formData.get() checks + .toString() coercions should be replaced with schema-validated parsing and standardized validation errors.

Suggested direction
+// Parse once from form data
+const body = Object.fromEntries((await request.formData()).entries());
+
+// Validate with a grant_type-driven schema (authorization_code | refresh_token)
+// and return serviceErrorResponse(requestBodySchemaValidationError(...)) on failure.
+
-const formData = await request.formData();
-const grantType = formData.get('grant_type');
-...

As per coding guidelines, "Request body (POST/PUT/PATCH) should use Zod schemas with safeParse and return requestBodySchemaValidationError if validation fails."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/api/`(server)/ee/oauth/token/route.ts around lines 17 -
101, Replace the manual formData.get() checks and .toString() coercions in the
token route with Zod schema validation using safeParse: define separate request
schemas (e.g., AuthorizationCodeRequestSchema and RefreshTokenRequestSchema)
that require the fields used by verifyAndExchangeCode and
verifyAndRotateRefreshToken, call schema.safeParse(parsedForm) in each grant
branch, and if parsing fails return requestBodySchemaValidationError with the
Zod errors; on success, pass the typed values (no .toString coercions) into
verifyAndExchangeCode and verifyAndRotateRefreshToken and keep the existing
response shapes.
🧹 Nitpick comments (1)
packages/db/prisma/schema.prisma (1)

541-552: Add an index for clientId on OAuthToken to support revocation paths.

verifyAndRotateRefreshToken performs client-wide token deletes by clientId; without an index this can become expensive at scale.

📈 Suggested schema update
 model OAuthToken {
   hash       String   `@id` // hashSecret(rawToken secret portion)
   clientId   String
   client     OAuthClient `@relation`(fields: [clientId], references: [id], onDelete: Cascade)
   userId     String
   user       User        `@relation`(fields: [userId], references: [id], onDelete: Cascade)
   scope      String      `@default`("")
   resource   String? // RFC 8707: canonical URI of the target resource server
   expiresAt  DateTime
   createdAt  DateTime    `@default`(now())
   lastUsedAt DateTime?
+
+  @@index([clientId])
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/prisma/schema.prisma` around lines 541 - 552, Add a database
index on OAuthToken.clientId to speed up client-wide deletes used by
verifyAndRotateRefreshToken: update the OAuthToken model (symbol: OAuthToken) to
add an index for the clientId field (symbol: clientId), then run the Prisma
migration (or prisma db push) and regenerate the Prisma client so the change is
applied and queries/deletes by clientId become efficient.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/app/oauth/authorize/page.tsx`:
- Around line 52-54: The current check of
client.redirectUris.includes(redirect_uri) allows arbitrary schemes and later
forwards non-http(s) callbacks to /oauth/complete; tighten validation by parsing
redirect_uri (using the URL constructor or equivalent) and allow only http:,
https:, or an explicit custom-protocol allowlist (e.g., ["cursor:", "claude:"])
before forwarding to /oauth/complete. Update the logic around
client.redirectUris, redirect_uri, and the code paths that build/return the
/oauth/complete handoff (the branches that currently forward non-http(s)
callbacks) to reject or return an error for any scheme not in the allowlist, and
ensure comparisons still confirm redirect_uri is an exact registered URI in
client.redirectUris.

In `@packages/web/src/ee/features/oauth/server.test.ts`:
- Around line 215-229: The test and implementation currently perform client-wide
revocation (calling prisma.oAuthToken.deleteMany and
prisma.oAuthRefreshToken.deleteMany) when a refresh token lookup returns null in
verifyAndRotateRefreshToken; change the logic so that when
prisma.oAuthRefreshToken.findUnique returns null you simply return { error:
'invalid_grant' } without triggering clientId-wide deleteMany calls, or if you
must revoke, restrict deletion to the specific token family/user (e.g., by
tokenFamily or userId) only when that identity is verifiably known; update
verifyAndRotateRefreshToken to guard or remove the
prisma.oAuthToken.deleteMany/prisma.oAuthRefreshToken.deleteMany branch
triggered on a null refresh token and adjust the test to expect no bulk
deletions.

In `@packages/web/src/ee/features/oauth/server.ts`:
- Around line 106-139: Replace the separate delete + create steps with a single
atomic transaction: generate the access/refresh tokens (generateOAuthToken,
generateOAuthRefreshToken) as before, then call prisma.$transaction(async (tx)
=> { await tx.oAuthAuthorizationCode.delete({ where: { codeHash } }); await
tx.oAuthToken.create({ data: { hash, clientId, userId: authCode.userId,
resource: authCode.resource, expiresAt: new Date(Date.now() +
ACCESS_TOKEN_TTL_MS) } }); await tx.oAuthRefreshToken.create({ data: { hash:
refreshHash, clientId, userId: authCode.userId, resource: authCode.resource,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS) } }); }); and wrap the
transaction in the same Prisma.PrismaClientKnownRequestError P2025 check (from
the original delete catch) to return the invalid_grant error if the delete fails
(code already consumed), otherwise rethrow other errors.
- Around line 186-206: The transaction that deletes and recreates tokens can
throw Prisma's P2025 on concurrent refresh attempts; catch errors around the
prisma.$transaction([...]) call, detect Prisma's "P2025" (e.g. by checking
err.code or instanceof Prisma.PrismaClientKnownRequestError) from the
prisma.oAuthRefreshToken.delete step, and convert it to an OAuth-style error
response (invalid_grant) instead of letting the exception bubble as a 500.
Update the refresh handler in this file to wrap the prisma.$transaction call in
a try/catch, map P2025 to the same invalid_grant error path used elsewhere, and
rethrow or propagate other errors unchanged.
- Around line 156-167: rawRefreshToken is sliced without validating
OAUTH_REFRESH_TOKEN_PREFIX and the code currently deletes all tokens for
clientId when the lookup returns no row, enabling DoS; first check
rawRefreshToken.startsWith(OAUTH_REFRESH_TOKEN_PREFIX) and return { error:
'invalid_grant' } immediately if it does not, then compute hash via
hashSecret(rawRefreshToken.slice(OAUTH_REFRESH_TOKEN_PREFIX.length)) and lookup
prisma.oAuthRefreshToken.findUnique; do NOT run the prisma.$transaction
revoke-all block when existing is null — only perform revocation for confirmed
theft cases (e.g., when an existing token is found but other abuse signals are
present) so that a bogus token cannot trigger mass deletion for clientId.

In `@packages/web/src/withAuthV2.test.ts`:
- Around line 198-220: Tests currently rely on getAuthenticatedUser but never
enable entitlements, so the function may return early and tests pass for the
wrong reason; before calling getAuthenticatedUser() in each OAuth-focused test,
enable the entitlement feature used by the module (e.g., call the test helper
that turns on the entitlement flag or set the feature flag/environment variable
your code checks) so the logic exercises the OAuth token lookup and expiration
branches (references: getAuthenticatedUser, prisma.oAuthToken.findUnique,
prisma.apiKey.findUnique).

---

Duplicate comments:
In `@packages/web/src/app/api/`(server)/ee/oauth/revoke/route.ts:
- Around line 17-25: Replace manual formData parsing in the revoke route with
Zod validation: parse request.formData() into an object and run
requestBodySchema.safeParse(...) (reference requestBodySchema and
requestBodySchemaValidationError). If safeParse fails, return
serviceErrorResponse(requestBodySchemaValidationError(parseResult.error)); if it
succeeds, extract the token string from parseResult.data and call
revokeToken(token) (reference revokeToken) and return Response.json({ success:
true }) for the 200 path instead of new Response(null, ...). Ensure you
import/use serviceErrorResponse and requestBodySchemaValidationError where
needed.

In `@packages/web/src/app/api/`(server)/ee/oauth/token/route.ts:
- Around line 17-101: Replace the manual formData.get() checks and .toString()
coercions in the token route with Zod schema validation using safeParse: define
separate request schemas (e.g., AuthorizationCodeRequestSchema and
RefreshTokenRequestSchema) that require the fields used by verifyAndExchangeCode
and verifyAndRotateRefreshToken, call schema.safeParse(parsedForm) in each grant
branch, and if parsing fails return requestBodySchemaValidationError with the
Zod errors; on success, pass the typed values (no .toString coercions) into
verifyAndExchangeCode and verifyAndRotateRefreshToken and keep the existing
response shapes.

In `@packages/web/src/app/oauth/authorize/page.tsx`:
- Around line 64-98: handleAllow and handleDeny use the render-time session
(session!.user.id) but must authenticate at action execution; update both server
actions to call the appropriate auth wrapper (withAuthV2 or withOptionalAuthV2
from "@/withAuthV2") inside the action to obtain the up-to-date user info and
replace session!.user.id with the authenticated user id, and wrap the action
body with sew() for error handling; ensure generateAndStoreAuthCode is called
with the userId from the auth result and preserve the existing redirect logic
unchanged.
- Around line 34-44: The authorization parameter validation currently allows a
missing code_challenge_method; update the validation block in authorize/page.tsx
to treat code_challenge_method as required and to reject anything other than
'S256'. Specifically, alongside the existing checks for client_id, redirect_uri,
code_challenge and response_type, add/modify the check for code_challenge_method
so that if it's missing or not equal to 'S256' you return an ErrorPage (use the
same ErrorPage component) with a clear protocol error message referencing
code_challenge_method and that only "S256" is supported.

---

Nitpick comments:
In `@packages/db/prisma/schema.prisma`:
- Around line 541-552: Add a database index on OAuthToken.clientId to speed up
client-wide deletes used by verifyAndRotateRefreshToken: update the OAuthToken
model (symbol: OAuthToken) to add an index for the clientId field (symbol:
clientId), then run the Prisma migration (or prisma db push) and regenerate the
Prisma client so the change is applied and queries/deletes by clientId become
efficient.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f98e3fd4-11bd-4300-a792-5fb18db8e47e

📥 Commits

Reviewing files that changed from the base of the PR and between 633f0e0 and 502472e.

📒 Files selected for processing (14)
  • packages/db/prisma/migrations/20260304233124_add_oauth_refresh_tokens/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/shared/src/constants.ts
  • packages/shared/src/crypto.ts
  • packages/shared/src/index.server.ts
  • packages/web/src/__mocks__/prisma.ts
  • packages/web/src/app/api/(server)/ee/.well-known/oauth-authorization-server/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/revoke/route.ts
  • packages/web/src/app/api/(server)/ee/oauth/token/route.ts
  • packages/web/src/app/oauth/authorize/page.tsx
  • packages/web/src/ee/features/oauth/server.test.ts
  • packages/web/src/ee/features/oauth/server.ts
  • packages/web/src/withAuthV2.test.ts
  • packages/web/src/withAuthV2.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/web/src/withAuthV2.ts
  • packages/web/src/mocks/prisma.ts
  • packages/shared/src/index.server.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/web/src/app/oauth/authorize/page.tsx (2)

30-40: ⚠️ Potential issue | 🟠 Major

Require code_challenge_method and enforce S256 up front.

Line 30 currently permits a missing code_challenge_method; fail early at authorization-time instead of allowing a later flow failure.

🔧 Suggested fix
-    if (!client_id || !redirect_uri || !code_challenge || !response_type) {
+    if (!client_id || !redirect_uri || !code_challenge || !code_challenge_method || !response_type) {
         return <ErrorPage message="Missing required OAuth parameters." />;
     }

-    if (code_challenge_method && code_challenge_method !== 'S256') {
+    if (code_challenge_method !== 'S256') {
         return <ErrorPage message={`Unsupported code_challenge_method: ${code_challenge_method}. Only "S256" is supported.`} />;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/authorize/page.tsx` around lines 30 - 40, The
authorization page currently allows a missing code_challenge_method; update the
validation in the authorize handler (the block that checks client_id,
redirect_uri, code_challenge, response_type and code_challenge_method) to treat
code_challenge_method as required and to immediately return an ErrorPage when
it's missing or not equal to 'S256'. Specifically, add code_challenge_method to
the required-params check (the same conditional that returns "Missing required
OAuth parameters.") and keep the existing check that returns an error if
code_challenge_method !== 'S256', ensuring no flow continues without an explicit
'S256' value.

48-50: ⚠️ Potential issue | 🔴 Critical

Block unsafe redirect URI schemes before rendering consent.

Line 48 only checks string membership in registered URIs. Add protocol validation (http:, https:, and explicit native-app allowlist such as cursor:/claude:) to prevent dangerous schemes from entering the authorize flow.

🔒 Suggested hardening
+    let parsedRedirectUri: URL;
+    try {
+        parsedRedirectUri = new URL(redirect_uri);
+    } catch {
+        return <ErrorPage message="Invalid redirect_uri format." />;
+    }
+
+    const allowedCustomProtocols = new Set(['cursor:', 'claude:']);
+    const isWebProtocol = parsedRedirectUri.protocol === 'http:' || parsedRedirectUri.protocol === 'https:';
+    if (!isWebProtocol && !allowedCustomProtocols.has(parsedRedirectUri.protocol)) {
+        return <ErrorPage message={`Unsupported redirect_uri protocol: ${parsedRedirectUri.protocol}`} />;
+    }
+
     if (!client.redirectUris.includes(redirect_uri)) {
         return <ErrorPage message="redirect_uri does not match the registered redirect URIs for this client." />;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/oauth/authorize/page.tsx` around lines 48 - 50, The
current check only verifies membership in client.redirectUris; before rendering
consent in the authorize flow, parse and validate the redirect_uri scheme and
reject unsafe schemes. Update the logic around the redirect URI check (the block
that uses client.redirectUris.includes(redirect_uri) and returns ErrorPage) to
first safely parse redirect_uri (handle parse errors), extract its protocol, and
allow only http:, https: or an explicit native-app allowlist (e.g., cursor:,
claude:); if the protocol is not in that allowlist or parsing fails, return an
ErrorPage with a clear message; after scheme validation, continue to verify that
redirect_uri is among client.redirectUris as currently done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/ee/features/oauth/actions.ts`:
- Around line 23-68: Re-validate clientId and redirectUri inside the server
actions: inside approveAuthorization and denyAuthorization, lookup the OAuth
client (e.g. via the same client-retrieval used elsewhere) and verify the
provided clientId exists and that the redirectUri exactly matches one of the
client's allowed redirect URIs before proceeding (keep using withAuthV2,
generateAndStoreAuthCode and resolveCallbackUrl after validation). Change
denyAuthorization to accept clientId (in signature and calls) and perform the
same client+redirectUri validation as approveAuthorization before returning an
error callback URL. Finally update the consentScreen.tsx call site to pass
clientId into denyAuthorization. Ensure all validation failures return a safe
error (do not perform code generation or redirect to arbitrary URIs).

---

Duplicate comments:
In `@packages/web/src/app/oauth/authorize/page.tsx`:
- Around line 30-40: The authorization page currently allows a missing
code_challenge_method; update the validation in the authorize handler (the block
that checks client_id, redirect_uri, code_challenge, response_type and
code_challenge_method) to treat code_challenge_method as required and to
immediately return an ErrorPage when it's missing or not equal to 'S256'.
Specifically, add code_challenge_method to the required-params check (the same
conditional that returns "Missing required OAuth parameters.") and keep the
existing check that returns an error if code_challenge_method !== 'S256',
ensuring no flow continues without an explicit 'S256' value.
- Around line 48-50: The current check only verifies membership in
client.redirectUris; before rendering consent in the authorize flow, parse and
validate the redirect_uri scheme and reject unsafe schemes. Update the logic
around the redirect URI check (the block that uses
client.redirectUris.includes(redirect_uri) and returns ErrorPage) to first
safely parse redirect_uri (handle parse errors), extract its protocol, and allow
only http:, https: or an explicit native-app allowlist (e.g., cursor:, claude:);
if the protocol is not in that allowlist or parsing fails, return an ErrorPage
with a clear message; after scheme validation, continue to verify that
redirect_uri is among client.redirectUris as currently done.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf8a55b8-f010-40ad-b3bf-08f46729dd95

📥 Commits

Reviewing files that changed from the base of the PR and between 502472e and 76c68af.

📒 Files selected for processing (5)
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts
  • packages/web/src/app/oauth/authorize/components/consentScreen.tsx
  • packages/web/src/app/oauth/authorize/page.tsx
  • packages/web/src/ee/features/oauth/actions.ts
  • packages/web/src/lib/posthogEvents.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/app/api/(server)/ee/oauth/register/route.ts

@brendan-kellam brendan-kellam merged commit fe37d4f into main Mar 5, 2026
8 checks passed
@brendan-kellam brendan-kellam deleted the bkellam/mcp-oauth-SOU-264 branch March 5, 2026 01:52
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