mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Merge branch 'wave/v124-ewe-oauth' into release/v1.24.0
This commit is contained in:
commit
391c78674e
6 files changed
+65
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");
|
||||
});
|
||||
});
|
||||
@@ -77,6 +77,39 @@ describe("POST /register", () => {
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("honours a valid OIDC application_type and echoes it back", async () => {
|
||||
const res = await POST(
|
||||
jsonReq({
|
||||
client_name: "Native app",
|
||||
application_type: "native",
|
||||
redirect_uris: ["http://127.0.0.1:8976/callback"],
|
||||
}) as never,
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
expect((await res.json()).application_type).toBe("native");
|
||||
});
|
||||
|
||||
it("defaults application_type to web when omitted", async () => {
|
||||
const res = await POST(
|
||||
jsonReq({
|
||||
redirect_uris: ["https://chatgpt.com/cb"],
|
||||
}) as never,
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
expect((await res.json()).application_type).toBe("web");
|
||||
});
|
||||
|
||||
it("rejects an invalid application_type with invalid_client_metadata", async () => {
|
||||
const res = await POST(
|
||||
jsonReq({
|
||||
application_type: "robot",
|
||||
redirect_uris: ["https://chatgpt.com/cb"],
|
||||
}) as never,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
expect((await res.json()).error).toBe("invalid_client_metadata");
|
||||
});
|
||||
|
||||
it("returns 503 when the API is globally disabled (M4)", async () => {
|
||||
vi.mocked(isApiGloballyEnabled).mockResolvedValue(false);
|
||||
const res = await POST(
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,12 @@ const registrationSchema = z.object({
|
||||
message: "redirect_uris must be HTTPS or http loopback URLs",
|
||||
}),
|
||||
client_name: z.string().min(1).max(200).optional(),
|
||||
// OIDC dynamic-registration `application_type` (forward-compat with OIDC-style
|
||||
// clients): honoured by echoing it back, validated to the closed set so a
|
||||
// malformed value is rejected cleanly rather than silently dropped. Absent ⇒
|
||||
// the OIDC default `web`. It does not alter the redirect-URI rules — those are
|
||||
// already shape-validated below and authoritatively matched at `/authorize`.
|
||||
application_type: z.enum(["web", "native"]).optional(),
|
||||
// Accepted-and-ignored RFC 7591 fields (we never read them but tolerate them).
|
||||
grant_types: z.array(z.string()).optional(),
|
||||
response_types: z.array(z.string()).optional(),
|
||||
@@ -111,6 +117,8 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
const applicationType = parsed.data.application_type ?? "web";
|
||||
|
||||
const client = registerDcrClient({
|
||||
clientName: parsed.data.client_name ?? "MCP client",
|
||||
redirectUris: parsed.data.redirect_uris,
|
||||
@@ -128,6 +136,7 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
client_id_issued_at: Math.floor(Date.now() / 1000),
|
||||
redirect_uris: client.redirectUris,
|
||||
client_name: client.clientName,
|
||||
application_type: applicationType,
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
|
||||
@@ -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