Regression introduced with OIDC_ISSUER_MCP. mcpIssuer() stripped the trailing slash — right for building the JWKS URL, wrong for the `iss` claim check, which jose compares by exact string. Authentik emits `.../application/o/shared-memory-mcp/` with the slash, so verification failed with "claim invalid: iss" even though issuer and audience were both correct. Before OIDC_ISSUER_MCP the issuer was passed to jwtVerify unstripped and only stripped when constructing the URL; collapsing both onto the stripped form is what broke it. mcpIssuer() now returns the value as configured, the JWKS URL strips locally, and the claim check accepts both spellings so correctness doesn't hinge on whether someone typed a trailing slash into an env var. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
35 lines
1.4 KiB
TypeScript
35 lines
1.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { env } from "@/lib/env";
|
|
import { mcpIssuer } from "@/lib/auth/jwt";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* RFC 9728 — OAuth 2.0 Protected Resource Metadata.
|
|
*
|
|
* MCP clients discover the authorization server (Authentik) via this
|
|
* endpoint after receiving a 401 with `WWW-Authenticate: resource_metadata=...`.
|
|
*/
|
|
export function GET() {
|
|
const resource = env().PUBLIC_URL.replace(/\/$/, "");
|
|
|
|
// The audience scope MUST be advertised. Authentik only evaluates a scope
|
|
// mapping when the client requests that scope by name, and the client only
|
|
// learns scope names from this document. Omit it and every access token
|
|
// arrives without `aud`, which jwt.ts rejects as "claim invalid: aud".
|
|
const audienceScope =
|
|
env().OIDC_AUDIENCE_SCOPE ?? `aud-${env().OIDC_AUDIENCE}`;
|
|
|
|
return NextResponse.json({
|
|
resource,
|
|
// The MCP application's issuer, which is not necessarily the Web UI's —
|
|
// see mcpIssuer(). Advertising the wrong one sends clients to a discovery
|
|
// document whose tokens this endpoint will then reject on `iss`.
|
|
authorization_servers: [mcpIssuer()], // as configured, slash and all
|
|
scopes_supported: ["openid", "profile", "email", audienceScope],
|
|
bearer_methods_supported: ["header"],
|
|
resource_documentation: `${resource}/`,
|
|
});
|
|
}
|