mirror of
https://github.com/run-llama/mcp-nextjs.git
synced 2026-06-30 21:57:55 -04:00
Allow PKCE instead of client secret for public clients like VSCode and Claude.ai
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "AuthCode" ADD COLUMN "codeChallenge" TEXT,
|
||||
ADD COLUMN "codeChallengeMethod" TEXT;
|
||||
@@ -92,6 +92,8 @@ model AuthCode {
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
redirectUri String
|
||||
codeChallenge String?
|
||||
codeChallengeMethod String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
|
||||
const redirect_uri = formData.get('redirect_uri') as string;
|
||||
const client_id = formData.get('client_id') as string;
|
||||
const client_secret = formData.get('client_secret') as string | null;
|
||||
const code_verifier = formData.get('code_verifier') as string | undefined;
|
||||
|
||||
console.log("Form data:", { grant_type, code, redirect_uri, client_id });
|
||||
|
||||
@@ -66,20 +67,6 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (client.clientSecret && client.clientSecret !== client_secret) {
|
||||
console.log("Invalid client_secret.", { client_id });
|
||||
return NextResponse.json({ error: 'Invalid client' }, {
|
||||
status: 401,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Found client:", client.id);
|
||||
|
||||
console.log("Finding auth code:", code);
|
||||
const authCode = await prisma.authCode.findUnique({ where: { code } });
|
||||
if (!authCode || authCode.clientId !== client.id || authCode.redirectUri !== redirect_uri) {
|
||||
@@ -108,6 +95,54 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
console.log("Auth code is valid.");
|
||||
|
||||
// PKCE validation
|
||||
let pkceValid = false;
|
||||
if (authCode.codeChallenge) {
|
||||
if (!code_verifier) {
|
||||
return NextResponse.json({ error: 'Missing code_verifier for PKCE' }, {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
});
|
||||
}
|
||||
if (authCode.codeChallengeMethod === 'S256') {
|
||||
const hash = require('crypto').createHash('sha256').update(code_verifier).digest();
|
||||
const base64url = hash.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
pkceValid = base64url === authCode.codeChallenge;
|
||||
} else if (authCode.codeChallengeMethod === 'plain' || !authCode.codeChallengeMethod) {
|
||||
pkceValid = code_verifier === authCode.codeChallenge;
|
||||
}
|
||||
if (!pkceValid) {
|
||||
return NextResponse.json({ error: 'Invalid code_verifier for PKCE' }, {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If PKCE is not present or not valid, require client secret for confidential clients
|
||||
if (!authCode.codeChallenge && client.clientSecret && client.clientSecret !== client_secret) {
|
||||
console.log("Invalid client_secret.", { client_id });
|
||||
return NextResponse.json({ error: 'Invalid client' }, {
|
||||
status: 401,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the auth code so it can't be used again
|
||||
console.log("Deleting auth code:", authCode.id);
|
||||
await prisma.authCode.delete({ where: { id: authCode.id } });
|
||||
|
||||
@@ -17,6 +17,8 @@ export default async function AuthorizePage({
|
||||
const redirectUri = params.redirect_uri as string;
|
||||
const responseType = params.response_type as string;
|
||||
const state = params.state as string;
|
||||
const code_challenge = params.code_challenge as string | undefined;
|
||||
const code_challenge_method = params.code_challenge_method as string | undefined;
|
||||
|
||||
if (!session || !session.user || !session.user.id) {
|
||||
const headersList = headers();
|
||||
@@ -98,6 +100,8 @@ export default async function AuthorizePage({
|
||||
clientId: client.id,
|
||||
userId: session.user.id,
|
||||
redirectUri: redirectUri,
|
||||
codeChallenge: code_challenge,
|
||||
codeChallengeMethod: code_challenge_method,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user