mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(mcp): identify the authorization-server issuer per RFC 9207
Advertise authorization_response_iss_parameter_supported in the authorization-server metadata and echo the iss parameter on every authorization response — both the success redirect carrying the code and the access_denied error redirect. A client can now pin the response to this server's issuer and reject a code substituted by a look-alike authorization server in a multi-server deployment. Additive and backward-compatible: existing clients that ignore iss are unaffected; PKCE S256-only, audience binding, redirect-URI exact match and the rate limits are untouched.
This commit is contained in:
1 parent
fb986bc09d
commit
01e2373255
4 files changed
+23
No files matched your search
@@ -286,6 +286,8 @@ describe("POST /authorize — decision", () => {
|
||||
const code = new URL(loc).searchParams.get("code");
|
||||
expect(code).toMatch(/^hlac_/);
|
||||
expect(new URL(loc).searchParams.get("state")).toBe("xyz");
|
||||
// RFC 9207 — the issuer identifier is echoed on the success redirect.
|
||||
expect(new URL(loc).searchParams.get("iss")).toBe("https://health.example");
|
||||
});
|
||||
|
||||
it("302s with access_denied on deny", async () => {
|
||||
@@ -305,5 +307,7 @@ describe("POST /authorize — decision", () => {
|
||||
expect(res.status).toBe(302);
|
||||
const loc = res.headers.get("location") ?? "";
|
||||
expect(new URL(loc).searchParams.get("error")).toBe("access_denied");
|
||||
// RFC 9207 — the issuer is echoed on the error redirect too.
|
||||
expect(new URL(loc).searchParams.get("iss")).toBe("https://health.example");
|
||||
});
|
||||
});
|
||||
@@ -414,10 +414,16 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
}
|
||||
const { v } = result;
|
||||
|
||||
// RFC 9207 — the authorization-server issuer identifier. Echoed on EVERY
|
||||
// authorization response (success AND error) so the client can pin the
|
||||
// response to this AS and reject a code from a substituted one.
|
||||
const iss = resolveBaseOrigin(request.url);
|
||||
|
||||
if (params.decision !== "allow") {
|
||||
annotate({ action: { name: "mcp.oauth.authorize.denied" } });
|
||||
return redirectBack(v.redirectUri, {
|
||||
error: "access_denied",
|
||||
iss,
|
||||
...(v.state !== undefined ? { state: v.state } : {}),
|
||||
});
|
||||
}
|
||||
@@ -450,6 +456,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
|
||||
return redirectBack(v.redirectUri, {
|
||||
code,
|
||||
iss,
|
||||
...(v.state !== undefined ? { state: v.state } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,6 +230,12 @@ describe("metadata + audience", () => {
|
||||
expect(as.grant_types_supported).toContain("refresh_token");
|
||||
});
|
||||
|
||||
it("AS metadata advertises the RFC 9207 iss parameter and a matching issuer", () => {
|
||||
const as = authorizationServerMetadata();
|
||||
expect(as.authorization_response_iss_parameter_supported).toBe(true);
|
||||
expect(as.issuer).toBe("https://health.example");
|
||||
});
|
||||
|
||||
it("the WWW-Authenticate challenge points at the PRM", () => {
|
||||
expect(wwwAuthenticateChallenge()).toMatch(
|
||||
/resource_metadata="https:\/\/health\.example\/\.well-known\/oauth-protected-resource"/,
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface AuthorizationServerMetadata {
|
||||
token_endpoint_auth_methods_supported: string[];
|
||||
code_challenge_methods_supported: string[];
|
||||
client_id_metadata_document_supported: boolean;
|
||||
authorization_response_iss_parameter_supported: boolean;
|
||||
}
|
||||
|
||||
/** RFC 9728 — the Protected Resource Metadata for the `/mcp` resource. */
|
||||
@@ -76,6 +77,11 @@ export function authorizationServerMetadata(
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
// CIMD is the preferred registration path (SEP-991); DCR is the fallback.
|
||||
client_id_metadata_document_supported: true,
|
||||
// RFC 9207 — the authorization response carries an `iss` parameter the
|
||||
// client validates against this metadata's `issuer`, so a code from a
|
||||
// look-alike AS in a multi-AS deployment cannot be substituted (mix-up
|
||||
// defence). The `/authorize` redirect always includes it.
|
||||
authorization_response_iss_parameter_supported: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user