import { createClient } from '@/lib/supabase/server' import { NextResponse } from 'next/server' import { createAuthCode } from '@/lib/auth/oauth-codes' /** * OAuth 2.0 Authorization Endpoint. * * GET → show consent page (or redirect to login) * POST → process consent, create auth code, redirect to callback * * The API key is NOT created here — it's created in the token endpoint * after PKCE verification, preventing orphaned keys on abandoned flows. */ // Allowed redirect URI patterns — prevent open redirect attacks const ALLOWED_REDIRECT_PATTERNS = [ /^https:\/\/claude\.ai\/api\//, // Claude.ai API callbacks (connector IDs vary in path) /^https:\/\/claude\.com\/api\//, // Claude.com API callbacks /^http:\/\/localhost(:\d+)?\//, // Local development /^http:\/\/127\.0\.0\.1(:\d+)?\//, // Local development ] function isAllowedRedirectUri(uri: string): boolean { return ALLOWED_REDIRECT_PATTERNS.some((pattern) => pattern.test(uri)) } function buildLoginRedirect(request: Request): Response { const url = new URL(request.url) const next = `${url.pathname}${url.search}` return NextResponse.redirect( new URL(`/login?next=${encodeURIComponent(next)}`, url.origin) ) } function errorRedirect(redirectUri: string, state: string | null, error: string, desc: string): Response { const url = new URL(redirectUri) url.searchParams.set('error', error) url.searchParams.set('error_description', desc) if (state) url.searchParams.set('state', state) return NextResponse.redirect(url.toString()) } /** * GET /api/mcp-oauth/authorize — show consent page */ export async function GET(request: Request) { const url = new URL(request.url) const redirectUri = url.searchParams.get('redirect_uri') const state = url.searchParams.get('state') const codeChallenge = url.searchParams.get('code_challenge') const codeChallengeMethod = url.searchParams.get('code_challenge_method') || 'S256' const responseType = url.searchParams.get('response_type') if (responseType !== 'code') { return NextResponse.json( { error: 'unsupported_response_type' }, { status: 400 } ) } if (!redirectUri) { return NextResponse.json( { error: 'invalid_request', error_description: 'redirect_uri is required' }, { status: 400 } ) } // Validate redirect_uri against allowlist (prevents open redirect) if (!isAllowedRedirectUri(redirectUri)) { return NextResponse.json( { error: 'invalid_request', error_description: 'redirect_uri is not allowed' }, { status: 400 } ) } if (codeChallengeMethod !== 'S256') { return NextResponse.json( { error: 'invalid_request', error_description: 'Only S256 code_challenge_method is supported' }, { status: 400 } ) } // Check if user is logged in const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) { return buildLoginRedirect(request) } // Get company name for the consent page const { data: settings } = await supabase .from('company_settings') .select('company_name') .eq('user_id', user.id) .single() const companyName = settings?.company_name || user.email // Render consent page const html = ` Anslut MCP-klient — gnubok

Anslut MCP-klient

En extern applikation vill ansluta till ditt gnubok-konto.

${escapeHtml(companyName)}
` return new Response(html, { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }) } /** * POST /api/mcp-oauth/authorize — process consent, issue auth code */ export async function POST(request: Request) { const url = new URL(request.url) const redirectUri = url.searchParams.get('redirect_uri') const state = url.searchParams.get('state') const codeChallenge = url.searchParams.get('code_challenge') || '' if (!redirectUri) { return NextResponse.json({ error: 'invalid_request' }, { status: 400 }) } if (!isAllowedRedirectUri(redirectUri)) { return NextResponse.json( { error: 'invalid_request', error_description: 'redirect_uri is not allowed' }, { status: 400 } ) } // Check auth const supabase = await createClient() const { data: { user } } = await supabase.auth.getUser() if (!user) { return buildLoginRedirect(request) } // Parse form body const formData = await request.formData() const consent = formData.get('consent') if (consent !== 'allow') { return errorRedirect(redirectUri, state, 'access_denied', 'User denied the request') } // Create auth code with userId (NO API key — that's created at /token after PKCE) const code = createAuthCode({ userId: user.id, codeChallenge, redirectUri, }) // Redirect to callback with the code const callbackUrl = new URL(redirectUri) callbackUrl.searchParams.set('code', code) if (state) callbackUrl.searchParams.set('state', state) return NextResponse.redirect(callbackUrl.toString()) } function escapeHtml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') }