Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-mcp/src/consent.ts
Snapshot at 34e3175
1import type { AuthRequest, OAuthHelpers } from "@cloudflare/workers-oauth-provider";
2import { PublicError } from "./core";
3
4/** D8 requires exact registration matching, including loopback ports, before consent/enrollment. */
5export async function requireRegisteredRedirect(oauth: OAuthHelpers, request: AuthRequest) {
6 const client = await oauth.lookupClient(request.clientId);
7 if (!client?.redirectUris.includes(request.redirectUri)) {
8 throw new PublicError(400, "Redirect URI must exactly match client registration.");
9 }
10}
11
12/** Emit a browser response, never fetch its Location. Preserve the registered callback and query. */
13export function consentRedirect(
14 request: AuthRequest,
15 redirectTo: string,
16 headers: Headers,
17 decision: "approve" | "deny",
18): Response {
19 const expected = new URL(request.redirectUri);
20 if (decision === "approve") {
21 const code = new URL(redirectTo).searchParams.get("code");
22 if (!code) throw new PublicError(502, "OAuth consent redirect refused.");
23 expected.searchParams.set("code", code);
24 } else {
25 for (const key of ["error", "error_description", "error_uri", "state", "iss"]) {
26 expected.searchParams.delete(key);
27 }
28 expected.searchParams.set("error", "access_denied");
29 }
30 if (request.state) expected.searchParams.set("state", request.state);
31 if (request.issuer) expected.searchParams.set("iss", request.issuer);
32 // Only the OAuth result fields may differ from the exact registered redirect_uri.
33 // Comparing the whole generated URL also rejects changed paths, query values, fragments,
34 // duplicate result fields, or extra parameters in a wrapped provider's response.
35 if (redirectTo !== expected.href) throw new PublicError(502, "OAuth consent redirect refused.");
36 headers.set("location", redirectTo);
37 return new Response(null, { status: 302, headers });
38}
39