@kieksme/mcp-http-kit 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +197 -2
- package/SECURITY.md +19 -0
- package/dist/auth/bearer.d.ts +14 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +43 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/context.d.ts +7 -0
- package/dist/auth/context.d.ts.map +1 -0
- package/dist/auth/context.js +46 -0
- package/dist/auth/context.js.map +1 -0
- package/dist/auth/index.d.ts +25 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +34 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oauth.d.ts +25 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +161 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +201 -0
- package/dist/config.js.map +1 -0
- package/dist/http/app.d.ts +32 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +127 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/host-guard.d.ts +22 -0
- package/dist/http/host-guard.d.ts.map +1 -0
- package/dist/http/host-guard.js +46 -0
- package/dist/http/host-guard.js.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +4 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/node.d.ts +36 -0
- package/dist/http/node.d.ts.map +1 -0
- package/dist/http/node.js +74 -0
- package/dist/http/node.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/start.d.ts +16 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +84 -0
- package/dist/start.js.map +1 -0
- package/dist/test/helpers.d.ts +35 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/helpers.js +171 -0
- package/dist/test/helpers.js.map +1 -0
- package/dist/test/index.d.ts +2 -0
- package/dist/test/index.d.ts.map +1 -0
- package/dist/test/index.js +2 -0
- package/dist/test/index.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upstream.d.ts +17 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +49 -0
- package/dist/upstream.js.map +1 -0
- package/docs/migration.md +125 -0
- package/package.json +87 -4
- package/templates/Dockerfile +46 -0
- package/templates/railway.json +13 -0
- package/templates/server.json +79 -0
- package/templates/smithery.yaml +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kieks.me
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,198 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@kieksme/mcp-http-kit`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared **Streamable HTTP** transport, **auth** (bearer + OAuth 2.1 resource server), and **deploy templates** for kieksme MCP servers.
|
|
4
|
+
|
|
5
|
+
Use this so every server speaks the same env scheme, passes Railway healthchecks, and can be listed on MCP registries without copy-pasting HTTP glue.
|
|
6
|
+
|
|
7
|
+
## SDK decision (v2 only)
|
|
8
|
+
|
|
9
|
+
This kit targets the **MCP TypeScript SDK v2** packages:
|
|
10
|
+
|
|
11
|
+
| Package | Role |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `@modelcontextprotocol/server` | `McpServer`, `createMcpHandler` |
|
|
14
|
+
| `@modelcontextprotocol/node` | `toNodeHandler`, Node Streamable HTTP |
|
|
15
|
+
|
|
16
|
+
**Why not v1 / dual adapters?** Upstream marked v2 as the stable release line implementing the **2026-07-28** spec; `@modelcontextprotocol/sdk` is the legacy monolith. Newer kieksme servers (`mcp-enhancv`, `mcp-balena`) already use v2. Older ones (`mcp-contabo`, `mcp-listmonk`, `mcp-swetrix`, `mcp-hashnode`) should migrate to v2 rather than the kit carrying a forever-v1 adapter. See [`docs/migration.md`](./docs/migration.md).
|
|
17
|
+
|
|
18
|
+
Peer dependencies: install `@modelcontextprotocol/server` and `@modelcontextprotocol/node` in each server.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @kieksme/mcp-http-kit
|
|
24
|
+
pnpm add @modelcontextprotocol/server @modelcontextprotocol/node
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Node **22.14+**, ESM only, MIT.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
33
|
+
import { loadConfig, startMcpServer } from '@kieksme/mcp-http-kit';
|
|
34
|
+
|
|
35
|
+
function createServer(ctx) {
|
|
36
|
+
const server = new McpServer({ name: 'demo-mcp', version: '1.0.0' });
|
|
37
|
+
// ctx.auth.mode / ctx.auth.scopes available to tool handlers
|
|
38
|
+
return server;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = loadConfig(process.env, { realm: 'demo-mcp' });
|
|
42
|
+
await startMcpServer({ createServer, config });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `MCP_TRANSPORT=stdio` → stdio (default)
|
|
46
|
+
- `MCP_TRANSPORT=http` → stateless Streamable HTTP (`POST /mcp`)
|
|
47
|
+
|
|
48
|
+
Example: [`examples/echo-server`](./examples/echo-server).
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
MCP_TRANSPORT=stdio pnpm example:echo
|
|
52
|
+
MCP_TRANSPORT=http MCP_AUTH_TOKEN=dev MCP_HTTP_HOST=127.0.0.1 pnpm example:echo
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Environment variables
|
|
56
|
+
|
|
57
|
+
| Variable | Default | Notes |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `MCP_TRANSPORT` | `stdio` | `stdio` \| `http` |
|
|
60
|
+
| `PORT` | `3000` | Preferred listen port (Railway). Fallback: `MCP_HTTP_PORT` |
|
|
61
|
+
| `MCP_HTTP_HOST` | `127.0.0.1` | Use `0.0.0.0` in containers |
|
|
62
|
+
| `MCP_HTTP_PATH` | `/mcp` | MCP endpoint |
|
|
63
|
+
| `MCP_HEALTH_PATH` | `/health` | Primary unauthenticated health path |
|
|
64
|
+
| `MCP_HEALTH_ALIASES` | — | Extra health paths (comma-separated), e.g. `/healthz` |
|
|
65
|
+
| `MCP_ALLOWED_HOSTS` | localhost trio | Required when binding beyond localhost |
|
|
66
|
+
| `MCP_ALLOWED_ORIGINS` | _(derived)_ | Optional absolute origins; else Origin hostname must be in hosts |
|
|
67
|
+
| `MCP_AUTH_MODE` | `bearer` | `bearer` \| `oauth` \| `none` |
|
|
68
|
+
| `MCP_AUTH_TOKEN` | — | Bearer secrets; **comma-separated** for rotation |
|
|
69
|
+
| `MCP_AUTH_ALLOW_NONE` | `false` | Opt-in for `none` off localhost (public read-only) |
|
|
70
|
+
| `MCP_AUTH_REALM` | `mcp` | WWW-Authenticate realm (or pass `loadConfig(..., { realm })`) |
|
|
71
|
+
| `MCP_HTTP_ENABLE_SESSION_METHODS` | `false` | If true, GET/DELETE on MCP path are forwarded (else **405**) |
|
|
72
|
+
| `MCP_OAUTH_RESOURCE` | — | Canonical resource URL (oauth) |
|
|
73
|
+
| `MCP_OAUTH_AUTHORIZATION_SERVERS` | — | Comma-separated AS issuer URLs |
|
|
74
|
+
| `MCP_OAUTH_AUDIENCES` | resource | Expected JWT `aud` / resource |
|
|
75
|
+
| `MCP_OAUTH_JWKS_URL` | _(discovered)_ | Optional JWKS override |
|
|
76
|
+
| `MCP_OAUTH_REQUIRED_SCOPES` | _(none)_ | All must be present |
|
|
77
|
+
| `MCP_OAUTH_SCOPES_SUPPORTED` | required | Advertised in RFC 9728 metadata |
|
|
78
|
+
|
|
79
|
+
**Deprecated aliases** (still work, warn once): `MCP_HTTP_AUTH_TOKEN`, `MCP_SERVER_AUTH_TOKEN`, `MCP_HTTP_ALLOWED_HOSTS`, `MCP_HTTP_ALLOWED_ORIGINS`, `MCP_HTTP_ENDPOINT`, `HEALTH_ENDPOINT`.
|
|
80
|
+
|
|
81
|
+
## Auth modes
|
|
82
|
+
|
|
83
|
+
### `bearer`
|
|
84
|
+
|
|
85
|
+
Constant-time compare against `MCP_AUTH_TOKEN` list. Missing/invalid → **401** + `WWW-Authenticate: Bearer realm="…"`.
|
|
86
|
+
|
|
87
|
+
### `oauth`
|
|
88
|
+
|
|
89
|
+
MCP server acts as an **OAuth 2.1 resource server** only (never an authorization server):
|
|
90
|
+
|
|
91
|
+
1. Serves RFC 9728 metadata at `/.well-known/oauth-protected-resource` (+ path-aware variant for `/mcp`)
|
|
92
|
+
2. Validates JWT access tokens via JWKS (issuer, audience/resource, expiry, scopes)
|
|
93
|
+
3. 401 responses include `resource_metadata="…"` so clients can discover the AS
|
|
94
|
+
4. Scopes are available on `ctx.auth.scopes` and as SDK `authInfo` for `requireScopes`
|
|
95
|
+
|
|
96
|
+
### `none`
|
|
97
|
+
|
|
98
|
+
Allowed on **localhost** binds, or with `MCP_AUTH_ALLOW_NONE=true` for intentional public read-only endpoints. Header-based upstream credentials are **refused** in this mode.
|
|
99
|
+
|
|
100
|
+
## Request pipeline (HTTP)
|
|
101
|
+
|
|
102
|
+
1. **`GET` health paths** (`MCP_HEALTH_PATH` + `MCP_HEALTH_ALIASES`) — no auth, **before** Host/Origin checks (Railway-friendly)
|
|
103
|
+
2. **OAuth well-known** — no auth, before Host checks
|
|
104
|
+
3. Host / Origin allowlist → **403**
|
|
105
|
+
4. MCP path; non-POST → **405** (unless session methods enabled)
|
|
106
|
+
5. Auth → **401**
|
|
107
|
+
6. Stateless `createMcpHandler` (body size limit applied)
|
|
108
|
+
|
|
109
|
+
Graceful **SIGTERM** / **SIGINT** shutdown is wired by `startMcpServer`.
|
|
110
|
+
|
|
111
|
+
### Health body hook
|
|
112
|
+
|
|
113
|
+
Default body is `{ "status": "ok" }`. Pass `getHealthInfo` to add fields (version, tool count, …). `status: "ok"` always wins:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await startMcpServer({
|
|
117
|
+
createServer,
|
|
118
|
+
config,
|
|
119
|
+
getHealthInfo: () => ({ version: '1.2.3', tools: 12 }),
|
|
120
|
+
});
|
|
121
|
+
// → { "version": "1.2.3", "tools": 12, "status": "ok" }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Aliases via env or `loadConfig(..., { healthPathAliases: ['/healthz'] })`.
|
|
125
|
+
|
|
126
|
+
## Framework-agnostic core
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { createMcpHttpApp, handleWithKitApp, createNodeHttpServer } from '@kieksme/mcp-http-kit';
|
|
130
|
+
|
|
131
|
+
const app = createMcpHttpApp({ createServer, config });
|
|
132
|
+
// Web Fetch API: app.fetch(request)
|
|
133
|
+
// Node: createNodeHttpServer({ app, config })
|
|
134
|
+
// Fastify/Express: handleWithKitApp(app, req.raw, reply.raw, config, req.body)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Upstream credentials (per request)
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { resolveUpstreamCredential } from '@kieksme/mcp-http-kit';
|
|
141
|
+
|
|
142
|
+
const { value } = resolveUpstreamCredential(ctx.headers, {
|
|
143
|
+
mode: 'header', // or 'env'
|
|
144
|
+
envVar: 'CONTABO_API_KEY',
|
|
145
|
+
headerName: 'X-Contabo-Api-Key',
|
|
146
|
+
authMode: ctx.auth.mode,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Test helpers
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createAppTestClient, createKitContractAssertions } from '@kieksme/mcp-http-kit/test';
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Covered contracts: 401 without token, 403 bad Host/Origin, `/health` without auth (incl. bad Host), initialize + `tools/list`.
|
|
157
|
+
|
|
158
|
+
## Railway deploy
|
|
159
|
+
|
|
160
|
+
1. Copy `templates/Dockerfile` and `templates/railway.json`
|
|
161
|
+
2. Set `MCP_TRANSPORT=http`, `MCP_HTTP_HOST=0.0.0.0`, `MCP_AUTH_TOKEN`, `MCP_ALLOWED_HOSTS=<your-public-host>`
|
|
162
|
+
3. Railway sets `PORT`; the kit reads it first
|
|
163
|
+
4. Healthcheck path is `/health` (no auth, Host-agnostic)
|
|
164
|
+
|
|
165
|
+
## Registry / Smithery
|
|
166
|
+
|
|
167
|
+
- `templates/server.json` — official MCP registry (`io.github.kieksme/<name>`, npm + OCI + `remotes` streamable-http)
|
|
168
|
+
- `templates/smithery.yaml` — container runtime, HTTP start command
|
|
169
|
+
|
|
170
|
+
## Public API
|
|
171
|
+
|
|
172
|
+
| Export | Purpose |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `loadConfig` / `resolveHealthPaths` | Env → `KitConfig` (+ legacy aliases) |
|
|
175
|
+
| `startMcpServer` | stdio or HTTP lifecycle (`getHealthInfo` supported) |
|
|
176
|
+
| `createMcpHttpApp` | Web-standard `{ fetch, close }` |
|
|
177
|
+
| `createNodeHttpServer` / `createNodeRequestListener` | `node:http` |
|
|
178
|
+
| `handleWithKitApp` | Fastify/Express bridge |
|
|
179
|
+
| `authenticateRequest` / bearer + oauth helpers | Auth primitives |
|
|
180
|
+
| `resolveUpstreamCredential` | env \| header upstream keys |
|
|
181
|
+
| `@kieksme/mcp-http-kit/test` | Contract test helpers |
|
|
182
|
+
|
|
183
|
+
### Git installs
|
|
184
|
+
|
|
185
|
+
`prepare` builds `dist/` when it is missing (typical for `github:` / git URL deps). Published npm tarballs already include `dist`, so prepare is a no-op there.
|
|
186
|
+
|
|
187
|
+
## Scripts
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pnpm lint
|
|
191
|
+
pnpm typecheck
|
|
192
|
+
pnpm test
|
|
193
|
+
pnpm build
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT © kieks.me
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the latest published release of `@kieksme/mcp-http-kit`.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please open a private security advisory on
|
|
10
|
+
[GitHub](https://github.com/kieksme/mcp-http-kit/security/advisories/new)
|
|
11
|
+
or email the maintainers. Do not file public issues for undisclosed vulnerabilities.
|
|
12
|
+
|
|
13
|
+
## Hardening notes
|
|
14
|
+
|
|
15
|
+
- Prefer `MCP_AUTH_MODE=bearer` or `oauth` for any non-localhost deployment.
|
|
16
|
+
- `MCP_AUTH_MODE=none` is refused unless the process binds to localhost or
|
|
17
|
+
`MCP_AUTH_ALLOW_NONE=true` is set explicitly (intended for public read-only demos).
|
|
18
|
+
- Keep `MCP_ALLOWED_HOSTS` / `MCP_ALLOWED_ORIGINS` tight when binding beyond loopback.
|
|
19
|
+
- Never commit bearer tokens or OAuth client secrets; use environment variables / Railway secrets.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthContext } from '../types.js';
|
|
2
|
+
export type BearerAuthResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
context: AuthContext;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
status: 401;
|
|
8
|
+
wwwAuthenticate: string;
|
|
9
|
+
body: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function extractBearerToken(authorization: string | null | undefined): string | undefined;
|
|
12
|
+
/** Constant-time check of Authorization: Bearer against one or more tokens. */
|
|
13
|
+
export declare function verifyBearerToken(authorization: string | null | undefined, tokens: string[], realm: string): BearerAuthResult;
|
|
14
|
+
//# sourceMappingURL=bearer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../../src/auth/bearer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAatE,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI/F;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,MAAM,EAAE,EAChB,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAuBlB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
function safeEqual(a, b) {
|
|
3
|
+
const left = Buffer.from(a);
|
|
4
|
+
const right = Buffer.from(b);
|
|
5
|
+
if (left.length !== right.length) {
|
|
6
|
+
// Still compare against something of equal length to reduce timing skew on length.
|
|
7
|
+
timingSafeEqual(left, left);
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
return timingSafeEqual(left, right);
|
|
11
|
+
}
|
|
12
|
+
export function extractBearerToken(authorization) {
|
|
13
|
+
if (!authorization)
|
|
14
|
+
return undefined;
|
|
15
|
+
const match = /^Bearer\s+(.+)$/i.exec(authorization.trim());
|
|
16
|
+
return match?.[1]?.trim() || undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Constant-time check of Authorization: Bearer against one or more tokens. */
|
|
19
|
+
export function verifyBearerToken(authorization, tokens, realm) {
|
|
20
|
+
const token = extractBearerToken(authorization);
|
|
21
|
+
if (!token) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
status: 401,
|
|
25
|
+
wwwAuthenticate: `Bearer realm="${realm}"`,
|
|
26
|
+
body: 'Unauthorized',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const accepted = tokens.some(expected => safeEqual(token, expected));
|
|
30
|
+
if (!accepted) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
status: 401,
|
|
34
|
+
wwwAuthenticate: `Bearer realm="${realm}", error="invalid_token"`,
|
|
35
|
+
body: 'Unauthorized',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
context: { mode: 'bearer', token, scopes: [] },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=bearer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearer.js","sourceRoot":"","sources":["../../src/auth/bearer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAO9C,SAAS,SAAS,CAAC,CAAS,EAAE,CAAS;IACrC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,mFAAmF;QACnF,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,aAAwC;IACzE,IAAI,CAAC,aAAa;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5D,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;AACzC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAC/B,aAAwC,EACxC,MAAgB,EAChB,KAAa;IAEb,MAAM,KAAK,GAAG,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAChD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,eAAe,EAAE,iBAAiB,KAAK,GAAG;YAC1C,IAAI,EAAE,cAAc;SACrB,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;IACrE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,eAAe,EAAE,iBAAiB,KAAK,0BAA0B;YACjE,IAAI,EAAE,cAAc;SACrB,CAAC;IACJ,CAAC;IACD,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;KAC/C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuthInfo } from '@modelcontextprotocol/server';
|
|
2
|
+
import type { AuthContext, KitConfig } from '../types.js';
|
|
3
|
+
/** Map kit auth into the SDK `AuthInfo` pass-through (scopes reach tool handlers). */
|
|
4
|
+
export declare function toAuthInfo(auth: AuthContext, config: KitConfig): AuthInfo | undefined;
|
|
5
|
+
/** Recover kit `AuthContext` from SDK `AuthInfo` (or fall back to none). */
|
|
6
|
+
export declare function fromAuthInfo(authInfo: AuthInfo | undefined): AuthContext;
|
|
7
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/auth/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAKpE,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,CAsBrF;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,CAkBxE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resourceMetadataUrl } from './oauth.js';
|
|
2
|
+
const KIT_AUTH_EXTRA_KEY = 'kieksme.auth';
|
|
3
|
+
/** Map kit auth into the SDK `AuthInfo` pass-through (scopes reach tool handlers). */
|
|
4
|
+
export function toAuthInfo(auth, config) {
|
|
5
|
+
if (auth.mode === 'none')
|
|
6
|
+
return undefined;
|
|
7
|
+
const token = auth.token ?? '';
|
|
8
|
+
const info = {
|
|
9
|
+
token,
|
|
10
|
+
clientId: auth.subject ?? auth.mode,
|
|
11
|
+
scopes: auth.scopes,
|
|
12
|
+
extra: {
|
|
13
|
+
[KIT_AUTH_EXTRA_KEY]: {
|
|
14
|
+
mode: auth.mode,
|
|
15
|
+
subject: auth.subject,
|
|
16
|
+
claims: auth.claims,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
if (config.auth.mode === 'oauth') {
|
|
21
|
+
info.resource = new URL(config.auth.resource);
|
|
22
|
+
info.resourceMetadataUrl = resourceMetadataUrl(config.auth.resource);
|
|
23
|
+
const exp = auth.claims?.exp;
|
|
24
|
+
if (typeof exp === 'number')
|
|
25
|
+
info.expiresAt = exp;
|
|
26
|
+
}
|
|
27
|
+
return info;
|
|
28
|
+
}
|
|
29
|
+
/** Recover kit `AuthContext` from SDK `AuthInfo` (or fall back to none). */
|
|
30
|
+
export function fromAuthInfo(authInfo) {
|
|
31
|
+
if (!authInfo) {
|
|
32
|
+
return { mode: 'none', scopes: [] };
|
|
33
|
+
}
|
|
34
|
+
const stamped = authInfo.extra?.[KIT_AUTH_EXTRA_KEY];
|
|
35
|
+
const subject = stamped?.subject ?? (authInfo.clientId !== 'bearer' && authInfo.clientId !== 'none'
|
|
36
|
+
? authInfo.clientId
|
|
37
|
+
: undefined);
|
|
38
|
+
return {
|
|
39
|
+
mode: stamped?.mode ?? (authInfo.scopes.length > 0 ? 'oauth' : 'bearer'),
|
|
40
|
+
token: authInfo.token,
|
|
41
|
+
scopes: authInfo.scopes,
|
|
42
|
+
...(subject ? { subject } : {}),
|
|
43
|
+
...(stamped?.claims ? { claims: stamped.claims } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/auth/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAE1C,sFAAsF;AACtF,MAAM,UAAU,UAAU,CAAC,IAAiB,EAAE,MAAiB;IAC7D,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAa;QACrB,KAAK;QACL,QAAQ,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI;QACnC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE;YACL,CAAC,kBAAkB,CAAC,EAAE;gBACpB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,IAAI,CAAC,MAAM;aACpB;SACF;KACF,CAAC;IACF,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACjC,IAAI,CAAC,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrE,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC;QAC7B,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,QAA8B;IACzD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACtC,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,kBAAkB,CAEtC,CAAC;IACd,MAAM,OAAO,GACX,OAAO,EAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,KAAK,MAAM;QACjF,CAAC,CAAC,QAAQ,CAAC,QAAQ;QACnB,CAAC,CAAC,SAAS,CAAC,CAAC;IACjB,OAAO;QACL,IAAI,EAAE,OAAO,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;QACxE,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACvD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AuthConfig, AuthContext } from '../types.js';
|
|
2
|
+
export type AuthenticateResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
context: AuthContext;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
status: 401;
|
|
8
|
+
wwwAuthenticate: string;
|
|
9
|
+
body: string;
|
|
10
|
+
contentType?: string;
|
|
11
|
+
};
|
|
12
|
+
export type AuthenticateOptions = {
|
|
13
|
+
auth: AuthConfig;
|
|
14
|
+
realm: string;
|
|
15
|
+
authorizationHeader: string | null | undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Authenticate an HTTP request according to kit auth mode.
|
|
19
|
+
* Callers should skip this for `/health` and well-known metadata routes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function authenticateRequest(options: AuthenticateOptions): Promise<AuthenticateResult>;
|
|
22
|
+
export declare function unauthorizedWwwAuthenticate(auth: AuthConfig, realm: string): string;
|
|
23
|
+
export { buildProtectedResourceMetadata, clearOAuthJwksCache, oauthWwwAuthenticate, resourceMetadataUrl, verifyOAuthAccessToken, } from './oauth.js';
|
|
24
|
+
export { extractBearerToken, verifyBearerToken } from './bearer.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI3D,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5F,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAChD,CAAC;AAEF;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsBnG;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnF;AAED,OAAO,EACL,8BAA8B,EAC9B,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { verifyBearerToken } from './bearer.js';
|
|
2
|
+
import { oauthWwwAuthenticate, verifyOAuthAccessToken } from './oauth.js';
|
|
3
|
+
/**
|
|
4
|
+
* Authenticate an HTTP request according to kit auth mode.
|
|
5
|
+
* Callers should skip this for `/health` and well-known metadata routes.
|
|
6
|
+
*/
|
|
7
|
+
export async function authenticateRequest(options) {
|
|
8
|
+
const { auth, realm, authorizationHeader } = options;
|
|
9
|
+
if (auth.mode === 'none') {
|
|
10
|
+
return { ok: true, context: { mode: 'none', scopes: [] } };
|
|
11
|
+
}
|
|
12
|
+
if (auth.mode === 'bearer') {
|
|
13
|
+
return verifyBearerToken(authorizationHeader, auth.tokens, realm);
|
|
14
|
+
}
|
|
15
|
+
const result = await verifyOAuthAccessToken(authorizationHeader, auth);
|
|
16
|
+
if (!result.ok) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
status: result.status,
|
|
20
|
+
wwwAuthenticate: result.wwwAuthenticate,
|
|
21
|
+
body: result.body,
|
|
22
|
+
contentType: 'application/json',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
export function unauthorizedWwwAuthenticate(auth, realm) {
|
|
28
|
+
if (auth.mode === 'oauth')
|
|
29
|
+
return oauthWwwAuthenticate(auth, 'invalid_token', 'Authentication required');
|
|
30
|
+
return `Bearer realm="${realm}"`;
|
|
31
|
+
}
|
|
32
|
+
export { buildProtectedResourceMetadata, clearOAuthJwksCache, oauthWwwAuthenticate, resourceMetadataUrl, verifyOAuthAccessToken, } from './oauth.js';
|
|
33
|
+
export { extractBearerToken, verifyBearerToken } from './bearer.js';
|
|
34
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAY1E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAA4B;IACpE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC;IAErD,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACzB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;IAC7D,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3B,OAAO,iBAAiB,CAAC,mBAAmB,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;IACvE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,eAAe,EAAE,MAAM,CAAC,eAAe;YACvC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,WAAW,EAAE,kBAAkB;SAChC,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,IAAgB,EAAE,KAAa;IACzE,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,oBAAoB,CAAC,IAAI,EAAE,eAAe,EAAE,yBAAyB,CAAC,CAAC;IACzG,OAAO,iBAAiB,KAAK,GAAG,CAAC;AACnC,CAAC;AAED,OAAO,EACL,8BAA8B,EAC9B,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AuthContext, OAuthAuthConfig } from '../types.js';
|
|
2
|
+
export type OAuthAuthResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
context: AuthContext;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
status: 401;
|
|
8
|
+
wwwAuthenticate: string;
|
|
9
|
+
body: string;
|
|
10
|
+
};
|
|
11
|
+
export type ProtectedResourceMetadata = {
|
|
12
|
+
resource: string;
|
|
13
|
+
authorization_servers: string[];
|
|
14
|
+
scopes_supported?: string[];
|
|
15
|
+
bearer_methods_supported: string[];
|
|
16
|
+
};
|
|
17
|
+
declare function resourceMetadataUrl(resource: string): string;
|
|
18
|
+
export declare function buildProtectedResourceMetadata(config: OAuthAuthConfig): ProtectedResourceMetadata;
|
|
19
|
+
export declare function oauthWwwAuthenticate(config: OAuthAuthConfig, error?: string, description?: string): string;
|
|
20
|
+
/** Validate a Bearer access token as an OAuth 2.1 resource server (JWT + JWKS). */
|
|
21
|
+
export declare function verifyOAuthAccessToken(authorization: string | null | undefined, config: OAuthAuthConfig): Promise<OAuthAuthResult>;
|
|
22
|
+
/** Clear JWKS cache (tests). */
|
|
23
|
+
export declare function clearOAuthJwksCache(): void;
|
|
24
|
+
export { resourceMetadataUrl };
|
|
25
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,wBAAwB,EAAE,MAAM,EAAE,CAAC;CACpC,CAAC;AAMF,iBAAS,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKrD;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,eAAe,GAAG,yBAAyB,CAOjG;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,eAAe,EACvB,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAMR;AAqFD,mFAAmF;AACnF,wBAAsB,sBAAsB,CAC1C,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACxC,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,eAAe,CAAC,CA0D1B;AAED,gCAAgC;AAChC,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify, } from 'jose';
|
|
2
|
+
const jwksCache = new Map();
|
|
3
|
+
function resourceMetadataUrl(resource) {
|
|
4
|
+
const url = new URL(resource);
|
|
5
|
+
// RFC 9728 path-aware well-known: insert /.well-known/oauth-protected-resource before path
|
|
6
|
+
const path = url.pathname === '/' ? '' : url.pathname.replace(/\/$/, '');
|
|
7
|
+
return `${url.origin}/.well-known/oauth-protected-resource${path}`;
|
|
8
|
+
}
|
|
9
|
+
export function buildProtectedResourceMetadata(config) {
|
|
10
|
+
return {
|
|
11
|
+
resource: config.resource,
|
|
12
|
+
authorization_servers: config.authorizationServers,
|
|
13
|
+
...(config.scopesSupported.length > 0 ? { scopes_supported: config.scopesSupported } : {}),
|
|
14
|
+
bearer_methods_supported: ['header'],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function oauthWwwAuthenticate(config, error, description) {
|
|
18
|
+
const metadata = resourceMetadataUrl(config.resource);
|
|
19
|
+
const parts = [`Bearer resource_metadata="${metadata}"`];
|
|
20
|
+
if (error)
|
|
21
|
+
parts.push(`error="${error}"`);
|
|
22
|
+
if (description)
|
|
23
|
+
parts.push(`error_description="${description.replace(/"/g, "'")}"`);
|
|
24
|
+
return parts.join(', ');
|
|
25
|
+
}
|
|
26
|
+
function parseScopes(payload) {
|
|
27
|
+
const scopeClaim = payload.scope;
|
|
28
|
+
if (typeof scopeClaim === 'string') {
|
|
29
|
+
return scopeClaim.split(/\s+/).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
const scp = payload.scp;
|
|
32
|
+
if (Array.isArray(scp)) {
|
|
33
|
+
return scp.filter((s) => typeof s === 'string');
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
function audiencesMatch(payload, expected) {
|
|
38
|
+
const aud = payload.aud;
|
|
39
|
+
const values = Array.isArray(aud) ? aud : aud ? [aud] : [];
|
|
40
|
+
// Also accept `resource` / `rsc` style claims used by some AS implementations.
|
|
41
|
+
const resourceClaim = payload.resource ?? payload.rsc;
|
|
42
|
+
if (typeof resourceClaim === 'string')
|
|
43
|
+
values.push(resourceClaim);
|
|
44
|
+
if (Array.isArray(resourceClaim)) {
|
|
45
|
+
for (const entry of resourceClaim) {
|
|
46
|
+
if (typeof entry === 'string')
|
|
47
|
+
values.push(entry);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return expected.some(want => values.includes(want));
|
|
51
|
+
}
|
|
52
|
+
async function resolveJwks(config) {
|
|
53
|
+
if (config.jwksUrl) {
|
|
54
|
+
const cached = jwksCache.get(config.jwksUrl);
|
|
55
|
+
if (cached)
|
|
56
|
+
return cached.getKey;
|
|
57
|
+
const getKey = createRemoteJWKSet(new URL(config.jwksUrl));
|
|
58
|
+
jwksCache.set(config.jwksUrl, { url: config.jwksUrl, getKey });
|
|
59
|
+
return getKey;
|
|
60
|
+
}
|
|
61
|
+
const issuer = config.authorizationServers[0];
|
|
62
|
+
if (!issuer) {
|
|
63
|
+
throw new Error('OAuth config missing authorizationServers');
|
|
64
|
+
}
|
|
65
|
+
const issuerUrl = issuer.endsWith('/') ? issuer : `${issuer}/`;
|
|
66
|
+
// Prefer OIDC discovery, then OAuth AS metadata.
|
|
67
|
+
const candidates = [
|
|
68
|
+
new URL('.well-known/openid-configuration', issuerUrl).href,
|
|
69
|
+
new URL('.well-known/oauth-authorization-server', issuerUrl).href,
|
|
70
|
+
];
|
|
71
|
+
for (const metadataUrl of candidates) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(metadataUrl, {
|
|
74
|
+
headers: { accept: 'application/json' },
|
|
75
|
+
signal: AbortSignal.timeout(10_000),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
continue;
|
|
79
|
+
const body = (await response.json());
|
|
80
|
+
if (!body.jwks_uri)
|
|
81
|
+
continue;
|
|
82
|
+
const cached = jwksCache.get(body.jwks_uri);
|
|
83
|
+
if (cached)
|
|
84
|
+
return cached.getKey;
|
|
85
|
+
const getKey = createRemoteJWKSet(new URL(body.jwks_uri));
|
|
86
|
+
jwksCache.set(body.jwks_uri, { url: body.jwks_uri, getKey });
|
|
87
|
+
return getKey;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// try next candidate
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Unable to resolve JWKS for authorization server ${issuer}; set MCP_OAUTH_JWKS_URL`);
|
|
94
|
+
}
|
|
95
|
+
function unauthorized(config, error, description) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
status: 401,
|
|
99
|
+
wwwAuthenticate: oauthWwwAuthenticate(config, error, description),
|
|
100
|
+
body: JSON.stringify({ error, error_description: description }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Validate a Bearer access token as an OAuth 2.1 resource server (JWT + JWKS). */
|
|
104
|
+
export async function verifyOAuthAccessToken(authorization, config) {
|
|
105
|
+
const header = authorization?.trim();
|
|
106
|
+
if (!header?.toLowerCase().startsWith('bearer ')) {
|
|
107
|
+
return unauthorized(config, 'invalid_token', 'Authentication required');
|
|
108
|
+
}
|
|
109
|
+
const token = header.slice(7).trim();
|
|
110
|
+
if (!token) {
|
|
111
|
+
return unauthorized(config, 'invalid_token', 'Authentication required');
|
|
112
|
+
}
|
|
113
|
+
let payload;
|
|
114
|
+
try {
|
|
115
|
+
const getKey = await resolveJwks(config);
|
|
116
|
+
const verified = await jwtVerify(token, getKey, {
|
|
117
|
+
issuer: config.authorizationServers,
|
|
118
|
+
clockTolerance: 30,
|
|
119
|
+
});
|
|
120
|
+
payload = verified.payload;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : 'Token validation failed';
|
|
124
|
+
return unauthorized(config, 'invalid_token', message);
|
|
125
|
+
}
|
|
126
|
+
if (!audiencesMatch(payload, config.audiences)) {
|
|
127
|
+
return unauthorized(config, 'invalid_token', 'Token audience does not match this resource');
|
|
128
|
+
}
|
|
129
|
+
const scopes = parseScopes(payload);
|
|
130
|
+
if (config.requiredScopes.length > 0) {
|
|
131
|
+
const missing = config.requiredScopes.filter(scope => !scopes.includes(scope));
|
|
132
|
+
if (missing.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
status: 401,
|
|
136
|
+
wwwAuthenticate: oauthWwwAuthenticate(config, 'insufficient_scope', `Missing scopes: ${missing.join(' ')}`),
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
error: 'insufficient_scope',
|
|
139
|
+
error_description: `Missing scopes: ${missing.join(' ')}`,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const subject = typeof payload.sub === 'string' ? payload.sub : undefined;
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
context: {
|
|
148
|
+
mode: 'oauth',
|
|
149
|
+
token,
|
|
150
|
+
scopes,
|
|
151
|
+
claims: payload,
|
|
152
|
+
...(subject ? { subject } : {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Clear JWKS cache (tests). */
|
|
157
|
+
export function clearOAuthJwksCache() {
|
|
158
|
+
jwksCache.clear();
|
|
159
|
+
}
|
|
160
|
+
export { resourceMetadataUrl };
|
|
161
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
|