diff --git a/src/app/.well-known/oauth-authorization-server/route.ts b/src/app/.well-known/oauth-authorization-server/route.ts new file mode 100644 index 0000000..5728b89 --- /dev/null +++ b/src/app/.well-known/oauth-authorization-server/route.ts @@ -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 { + return new Response(null, { status: 204, headers: CORS_HEADERS }); +} + +export async function GET(request: NextRequest): Promise { + 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 }); +} diff --git a/src/app/.well-known/oauth-protected-resource/mcp/route.ts b/src/app/.well-known/oauth-protected-resource/mcp/route.ts index 40cdd32..9b5e152 100644 --- a/src/app/.well-known/oauth-protected-resource/mcp/route.ts +++ b/src/app/.well-known/oauth-protected-resource/mcp/route.ts @@ -9,16 +9,6 @@ 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}`; const modifiedMetadata: Record = { ...clerkMetadata, @@ -26,7 +16,7 @@ const handler = async (request: NextRequest) => { authorization_servers: [baseUrl], authorization_endpoint: `${baseUrl}/authorize`, token_endpoint: `${baseUrl}/token`, - registration_endpoint: `${clerkBaseUrl}/oauth/register`, + registration_endpoint: `${baseUrl}/register`, scopes_supported: ["openid"], }; diff --git a/src/app/token/route.ts b/src/app/token/route.ts index ddb0962..c1b3034 100644 --- a/src/app/token/route.ts +++ b/src/app/token/route.ts @@ -88,8 +88,31 @@ export async function POST(request: NextRequest): Promise { 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)); + 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(