Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/app/.well-known/oauth-authorization-server/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextRequest } from "next/server";

const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export async function OPTIONS(): Promise<Response> {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}

export async function GET(request: NextRequest): Promise<Response> {
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_DOMAIN;

if (!clerkDomain) {
return Response.json(
{ error: "server_error", error_description: "Clerk domain not found" },
{ status: 500 },
);
}

const baseUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}`;

const metadata = {
issuer: baseUrl,
authorization_endpoint: `${baseUrl}/authorize`,
token_endpoint: `${baseUrl}/token`,
registration_endpoint: `${baseUrl}/register`,
jwks_uri: `https://${clerkDomain}/.well-known/jwks.json`,
scopes_supported: ["openid"],
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
token_endpoint_auth_methods_supported: [
"none",
"client_secret_post",
"client_secret_basic",
],
};

return Response.json(metadata, { headers: CORS_HEADERS });
}
12 changes: 1 addition & 11 deletions src/app/.well-known/oauth-protected-resource/mcp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,14 @@ const handler = async (request: NextRequest) => {
const clerkMetadata = await clerkResponse.json();

const baseUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}`;
const clerkDomain = process.env.NEXT_PUBLIC_CLERK_DOMAIN;

if (!clerkDomain) {
return Response.json(
{ error: "server_error", error_description: "Clerk domain not found" },
{ status: 500 },
);
}

const clerkBaseUrl = `https://${clerkDomain}`;
Comment thread
cursor[bot] marked this conversation as resolved.

const modifiedMetadata: Record<string, unknown> = {
...clerkMetadata,
resource: baseUrl,
authorization_servers: [baseUrl],
authorization_endpoint: `${baseUrl}/authorize`,
token_endpoint: `${baseUrl}/token`,
registration_endpoint: `${clerkBaseUrl}/oauth/register`,
registration_endpoint: `${baseUrl}/register`,
scopes_supported: ["openid"],
};

Expand Down
27 changes: 25 additions & 2 deletions src/app/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,31 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
const grantType = body.get("grant_type") as string;
console.debug("[token] start", { grantType });

// Validate client_id (required for both flows)
const clientId = body.get("client_id") as string | null;
// Extract client_id from body or Authorization: Basic header
let clientId = body.get("client_id") as string | null;
let clientSecret = body.get("client_secret") as string | null;

if (!clientId) {
const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Basic ")) {
try {
const decoded = atob(authHeader.slice(6));
const colonIdx = decoded.indexOf(":");
if (colonIdx !== -1) {
clientId = decodeURIComponent(decoded.slice(0, colonIdx));
clientSecret = decodeURIComponent(decoded.slice(colonIdx + 1));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Basic auth decoding mishandles form-encoded + as space

Low Severity

RFC 6749 Section 2.3.1 requires client_id and client_secret to be application/x-www-form-urlencoded before base64-encoding in the Basic auth header. Form encoding converts spaces to +, but decodeURIComponent does not decode + back to space — it only handles %XX sequences. If a credential ever contains a space, + would be kept literally instead of being decoded. The correct approach is to replace + with space before calling decodeURIComponent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 071b81e. Configure here.

params.set("client_id", clientId);
if (clientSecret) {
params.set("client_secret", clientSecret);
}
console.debug("[token] extracted client_id from Basic auth header");
}
} catch {
console.debug("[token] failed to decode Basic auth header");
}
}
}

if (!clientId) {
console.debug("[token] missing client_id");
return createErrorResponse(
Expand Down
Loading