@openparachute/vault 0.5.0-rc.3 → 0.5.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/oauth-discovery.test.ts +55 -0
- package/src/oauth-discovery.ts +24 -5
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { handleAuthorizationServer, handleProtectedResource } from "./oauth-discovery.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Regression: the per-vault discovery documents MUST advertise resource-narrowed
|
|
6
|
+
* `vault:<name>:<verb>` scopes, never broad `vault:<verb>`. A broad scope makes a
|
|
7
|
+
* spec-following MCP client (Claude) request `vault:read`, which the hub stamps
|
|
8
|
+
* `aud=vault`, which the per-vault MCP endpoint rejects ("audience mismatch:
|
|
9
|
+
* expected vault.<name>, got vault") — the live "Authorization with the MCP
|
|
10
|
+
* server failed" bug. See oauth-discovery.ts + auth.ts findBroadVaultScopes.
|
|
11
|
+
*/
|
|
12
|
+
describe("oauth-discovery per-vault scopes_supported is resource-narrowed", () => {
|
|
13
|
+
const req = new Request("https://hub.example.com/vault/default/.well-known/oauth-protected-resource");
|
|
14
|
+
|
|
15
|
+
test("protected-resource metadata advertises vault:<name>:<verb>, never broad", async () => {
|
|
16
|
+
const body = (await handleProtectedResource(req, "default").json()) as {
|
|
17
|
+
resource: string;
|
|
18
|
+
scopes_supported: string[];
|
|
19
|
+
};
|
|
20
|
+
expect(body.scopes_supported).toEqual([
|
|
21
|
+
"vault:default:read",
|
|
22
|
+
"vault:default:write",
|
|
23
|
+
"vault:default:admin",
|
|
24
|
+
]);
|
|
25
|
+
// No broad scope leaks through — vault rejects those on audience mismatch.
|
|
26
|
+
for (const s of body.scopes_supported) {
|
|
27
|
+
expect(s).not.toBe("vault:read");
|
|
28
|
+
expect(s).not.toBe("vault:write");
|
|
29
|
+
expect(s).not.toBe("vault:admin");
|
|
30
|
+
}
|
|
31
|
+
expect(body.resource).toContain("/vault/default/mcp");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("scopes are narrowed to the specific vault name", async () => {
|
|
35
|
+
const body = (await handleProtectedResource(req, "work-notes").json()) as {
|
|
36
|
+
scopes_supported: string[];
|
|
37
|
+
};
|
|
38
|
+
expect(body.scopes_supported).toEqual([
|
|
39
|
+
"vault:work-notes:read",
|
|
40
|
+
"vault:work-notes:write",
|
|
41
|
+
"vault:work-notes:admin",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("authorization-server forwarding doc also advertises narrowed scopes", async () => {
|
|
46
|
+
const body = (await handleAuthorizationServer(req, "default").json()) as {
|
|
47
|
+
scopes_supported: string[];
|
|
48
|
+
};
|
|
49
|
+
expect(body.scopes_supported).toEqual([
|
|
50
|
+
"vault:default:read",
|
|
51
|
+
"vault:default:write",
|
|
52
|
+
"vault:default:admin",
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/oauth-discovery.ts
CHANGED
|
@@ -28,8 +28,27 @@
|
|
|
28
28
|
|
|
29
29
|
import { getHubOrigin } from "./hub-jwt.ts";
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* OAuth scopes vault publishes through discovery, RESOURCE-NARROWED to the
|
|
33
|
+
* specific vault instance the metadata document describes.
|
|
34
|
+
*
|
|
35
|
+
* This MUST be narrowed (`vault:<name>:<verb>`), not broad (`vault:<verb>`).
|
|
36
|
+
* Post auth-unification (vault#282/#412), vault is a pure hub resource server
|
|
37
|
+
* that REJECTS broad `vault:<verb>` tokens (`findBroadVaultScopes` → 401) and
|
|
38
|
+
* requires `vault:<name>:<verb>` scopes carrying `aud=vault.<name>`. A
|
|
39
|
+
* spec-following MCP client (e.g. Claude) reads `scopes_supported` from this
|
|
40
|
+
* PRM and requests exactly those scopes; if we advertise broad `vault:read`
|
|
41
|
+
* the client gets a token stamped `aud=vault` and the per-vault MCP endpoint
|
|
42
|
+
* rejects it ("audience mismatch: expected vault.<name>, got vault") — the
|
|
43
|
+
* "Authorization with the MCP server failed" symptom. Advertising the narrowed
|
|
44
|
+
* shape makes the client request the scope vault will actually accept. The
|
|
45
|
+
* hub's RFC 8707 resource-binding narrows too, but only when the client echoes
|
|
46
|
+
* the `resource` param — advertising narrowed scopes here is the belt that
|
|
47
|
+
* works regardless. See scopes.ts for enforcement.
|
|
48
|
+
*/
|
|
49
|
+
function scopesSupportedFor(vaultName: string): string[] {
|
|
50
|
+
return [`vault:${vaultName}:read`, `vault:${vaultName}:write`, `vault:${vaultName}:admin`];
|
|
51
|
+
}
|
|
33
52
|
|
|
34
53
|
/**
|
|
35
54
|
* Public-facing base URL of the server. Honors `x-forwarded-*` headers so a
|
|
@@ -63,7 +82,7 @@ export function handleProtectedResource(req: Request, vaultName: string): Respon
|
|
|
63
82
|
return Response.json({
|
|
64
83
|
resource: `${base}${prefix}/mcp`,
|
|
65
84
|
authorization_servers: [getHubOrigin()],
|
|
66
|
-
scopes_supported:
|
|
85
|
+
scopes_supported: scopesSupportedFor(vaultName),
|
|
67
86
|
bearer_methods_supported: ["header"],
|
|
68
87
|
});
|
|
69
88
|
}
|
|
@@ -78,7 +97,7 @@ export function handleProtectedResource(req: Request, vaultName: string): Respon
|
|
|
78
97
|
* land here and discover the hub's actual endpoints; conformant clients that
|
|
79
98
|
* probe AS metadata directly at the vault path get the same answer.
|
|
80
99
|
*/
|
|
81
|
-
export function handleAuthorizationServer(_req: Request,
|
|
100
|
+
export function handleAuthorizationServer(_req: Request, vaultName: string): Response {
|
|
82
101
|
const hub = getHubOrigin();
|
|
83
102
|
return Response.json({
|
|
84
103
|
issuer: hub,
|
|
@@ -90,6 +109,6 @@ export function handleAuthorizationServer(_req: Request, _vaultName: string): Re
|
|
|
90
109
|
code_challenge_methods_supported: ["S256"],
|
|
91
110
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
92
111
|
token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
|
|
93
|
-
scopes_supported:
|
|
112
|
+
scopes_supported: scopesSupportedFor(vaultName),
|
|
94
113
|
});
|
|
95
114
|
}
|