@ni-c/mcp-hub 0.9.2 → 0.11.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +619 -0
  2. package/README.md +189 -64
  3. package/dist/admin.js +232 -8
  4. package/dist/admin.js.map +1 -1
  5. package/dist/auth/address.js +89 -0
  6. package/dist/auth/address.js.map +1 -0
  7. package/dist/auth/api-tokens.js +27 -0
  8. package/dist/auth/api-tokens.js.map +1 -0
  9. package/dist/auth/cimd.js +367 -0
  10. package/dist/auth/cimd.js.map +1 -0
  11. package/dist/auth/consent-page.js +4 -7
  12. package/dist/auth/consent-page.js.map +1 -1
  13. package/dist/auth/headers.js +22 -1
  14. package/dist/auth/headers.js.map +1 -1
  15. package/dist/auth/login-page.js +4 -7
  16. package/dist/auth/login-page.js.map +1 -1
  17. package/dist/auth/oidc/adapter.js +135 -0
  18. package/dist/auth/oidc/adapter.js.map +1 -0
  19. package/dist/auth/oidc/interactions.js +187 -0
  20. package/dist/auth/oidc/interactions.js.map +1 -0
  21. package/dist/auth/oidc/mount.js +144 -0
  22. package/dist/auth/oidc/mount.js.map +1 -0
  23. package/dist/auth/oidc/provider.js +440 -0
  24. package/dist/auth/oidc/provider.js.map +1 -0
  25. package/dist/auth/oidc/quirks.js +234 -0
  26. package/dist/auth/oidc/quirks.js.map +1 -0
  27. package/dist/auth/oidc/verifier.js +121 -0
  28. package/dist/auth/oidc/verifier.js.map +1 -0
  29. package/dist/auth/page.js +22 -0
  30. package/dist/auth/page.js.map +1 -1
  31. package/dist/auth/pinned-fetch.js +129 -0
  32. package/dist/auth/pinned-fetch.js.map +1 -0
  33. package/dist/auth/protected-resource.js +41 -0
  34. package/dist/auth/protected-resource.js.map +1 -0
  35. package/dist/auth/rate-limit.js +156 -0
  36. package/dist/auth/rate-limit.js.map +1 -0
  37. package/dist/auth/redirect-uri.js +90 -0
  38. package/dist/auth/redirect-uri.js.map +1 -0
  39. package/dist/auth/registration.js +146 -0
  40. package/dist/auth/registration.js.map +1 -0
  41. package/dist/auth/session.js +43 -0
  42. package/dist/auth/session.js.map +1 -0
  43. package/dist/auth/signed-token.js +49 -0
  44. package/dist/auth/signed-token.js.map +1 -0
  45. package/dist/auth/store.js +516 -5
  46. package/dist/auth/store.js.map +1 -1
  47. package/dist/auth/text.js +51 -0
  48. package/dist/auth/text.js.map +1 -0
  49. package/dist/config.js +184 -5
  50. package/dist/config.js.map +1 -1
  51. package/dist/docker-proxy/policy.js +1 -0
  52. package/dist/docker-proxy/policy.js.map +1 -1
  53. package/dist/docker-proxy/server.js +1 -0
  54. package/dist/docker-proxy/server.js.map +1 -1
  55. package/dist/elicitation.js +0 -0
  56. package/dist/elicitation.js.map +1 -0
  57. package/dist/forward.js +0 -0
  58. package/dist/forward.js.map +1 -0
  59. package/dist/health.js +22 -3
  60. package/dist/health.js.map +1 -1
  61. package/dist/hub.js +337 -29
  62. package/dist/hub.js.map +1 -1
  63. package/dist/index.js +218 -23
  64. package/dist/index.js.map +1 -1
  65. package/dist/limits.js +13 -1
  66. package/dist/limits.js.map +1 -1
  67. package/dist/mcp-limits.js +14 -2
  68. package/dist/mcp-limits.js.map +1 -1
  69. package/dist/proxy.js +262 -34
  70. package/dist/proxy.js.map +1 -1
  71. package/dist/stdio.js +97 -7
  72. package/dist/stdio.js.map +1 -1
  73. package/dist/subscriptions.js +236 -0
  74. package/dist/subscriptions.js.map +1 -0
  75. package/dist/supervisor.js +472 -28
  76. package/dist/supervisor.js.map +1 -1
  77. package/dist/timings.js +61 -0
  78. package/dist/timings.js.map +1 -0
  79. package/dist/tool-filter.js +59 -0
  80. package/dist/tool-filter.js.map +1 -0
  81. package/dist/transports/docker.js.map +1 -1
  82. package/dist/transports/stream.js +33 -20
  83. package/dist/transports/stream.js.map +1 -1
  84. package/dist/upstream/auth.js +435 -0
  85. package/dist/upstream/auth.js.map +1 -0
  86. package/dist/upstream/login.js +94 -0
  87. package/dist/upstream/login.js.map +1 -0
  88. package/dist/upstream/provider.js +286 -0
  89. package/dist/upstream/provider.js.map +1 -0
  90. package/dist/upstream/routes.js +97 -0
  91. package/dist/upstream/routes.js.map +1 -0
  92. package/package.json +23 -7
  93. package/dist/auth/provider.js +0 -380
  94. package/dist/auth/provider.js.map +0 -1
  95. package/dist/auth/routes.js +0 -223
  96. package/dist/auth/routes.js.map +0 -1
@@ -0,0 +1,440 @@
1
+ import Provider, { errors } from 'oidc-provider';
2
+ import { isSafeRedirectUri, redirectUriMatches } from '../redirect-uri.js';
3
+ import { clampDisplayName } from '../text.js';
4
+ import { createOidcAdapter } from './adapter.js';
5
+ import { HUB_SCOPE, installDiscoveryFixups, installThrowawaySecret } from './quirks.js';
6
+ /**
7
+ * The grant types this server has. `clientDefaults` below and the discovery
8
+ * document say the same thing; this is the list a client's own declaration is
9
+ * trimmed against, so it lives where both can be checked against it.
10
+ */
11
+ const SUPPORTED_GRANT_TYPES = ['authorization_code', 'refresh_token'];
12
+ /** Pinned by test/e2e.test.ts — a client that caches `expires_in` must not be
13
+ * handed a different number by the new authorization server. */
14
+ const ACCESS_TOKEN_TTL_S = 15 * 60;
15
+ const REFRESH_TOKEN_TTL_S = 30 * 24 * 3600;
16
+ const CODE_TTL_S = 10 * 60;
17
+ const SESSION_TTL_S = 30 * 60;
18
+ /** RFC 7523 §3 recommends a short window; the hub has always required five
19
+ * minutes, and a captured assertion is only useful inside it. */
20
+ const MAX_ASSERTION_LIFETIME_S = 5 * 60;
21
+ /** The single pseudo-account. The hub authenticates one operator with one
22
+ * password; there are no user accounts to tell apart. */
23
+ export const HUB_ACCOUNT_ID = 'mcp-hub-user';
24
+ /**
25
+ * The hub's authorization server.
26
+ *
27
+ * Routes are reconfigured onto mcp-hub's existing paths rather than mounted
28
+ * under a prefix: `#mountPath` is derived from the issuer (provider.js), and the
29
+ * hub's issuer is the origin root, so a prefixed mount would publish URLs the
30
+ * router does not answer on. See `mount.ts`.
31
+ */
32
+ export function buildOidcProvider(store, options) {
33
+ // NOT `.origin`. The hub publishes EXTERNAL_URL in `URL.href` form -- with a
34
+ // trailing slash -- in `iss`, `aud`, the AS metadata issuer and the PRM
35
+ // document, because claude.ai compares them byte for byte. oidc-provider
36
+ // accepts it: `mountPath` becomes '/' and pathFor() still yields '/token'.
37
+ const issuer = new URL(options.externalUrl).href;
38
+ /** Undefined for anything the hub would not serve; the caller turns that into
39
+ * `invalid_target` rather than minting an unusable token. */
40
+ const resolve = (indicator) => {
41
+ let parsed;
42
+ try {
43
+ parsed = new URL(indicator);
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ return options.resolveResource ? options.resolveResource(parsed) : parsed;
49
+ };
50
+ const configuration = {
51
+ adapter: createOidcAdapter(store, options.cimd),
52
+ // mcp-hub's paths, not oidc-provider's defaults. Every one of these is
53
+ // already in RESERVED_NAMES, so no MCP server can collide with them.
54
+ routes: {
55
+ authorization: '/authorize',
56
+ token: '/token',
57
+ revocation: '/revoke',
58
+ registration: '/register',
59
+ jwks: '/jwks',
60
+ end_session: '/session/end',
61
+ userinfo: '/userinfo'
62
+ },
63
+ // --- OAuth 2.1, no OIDC ------------------------------------------------
64
+ // `openid` cannot be removed from scopes_supported and the id_token alg
65
+ // list cannot be removed from the discovery document — configuration.js
66
+ // adds the first unconditionally and discovery.js the second. Documented
67
+ // rather than fought: nothing is issued for either.
68
+ responseTypes: ['code'],
69
+ scopes: [],
70
+ claims: {},
71
+ features: {
72
+ registration: { enabled: options.allowDynamicRegistration !== false },
73
+ // Enabled so a registration_access_token and registration_client_uri are
74
+ // issued at all -- but the hub serves GET/PUT/DELETE itself, mounted
75
+ // ahead of this. oidc-provider's version does not withdraw an approval
76
+ // when the redirect URIs move, does not hold an update to the same
77
+ // redirect-URI policy as the registration, and does not clamp a client
78
+ // name that arrives with newlines in it.
79
+ registrationManagement: { enabled: options.allowDynamicRegistration !== false, rotateRegistrationAccessToken: false },
80
+ revocation: { enabled: true },
81
+ userinfo: { enabled: false },
82
+ // Ships hardcoded /interaction/:uid routes that accept ANY password.
83
+ devInteractions: { enabled: false },
84
+ // Both are on by default and both would be ADVERTISED in the discovery
85
+ // document while their endpoints are not mounted -- a client that
86
+ // believed the document would get a 404. Neither is wanted: the hub has
87
+ // no browser session to end, and PAR solves a problem it does not have.
88
+ pushedAuthorizationRequests: { enabled: false },
89
+ rpInitiatedLogout: { enabled: false },
90
+ /**
91
+ * Enabled for what it advertises and for the client_id URL check — NOT
92
+ * for its fetcher. The adapter resolves metadata documents through the
93
+ * hub's own CimdResolver, which models/client.js consults first, and
94
+ * `allowFetch` refuses so the built-in path can never run.
95
+ *
96
+ * That is a deliberate choice, not an accident of ordering. The library
97
+ * caps the body at 5 KiB, times out at 2.5 s and refuses redirects; it
98
+ * does NOT pin the resolved address against DNS rebinding, enforce an
99
+ * origin allowlist, or rate-limit per origin. The hub does all three.
100
+ */
101
+ clientIdMetadataDocument: {
102
+ enabled: options.cimd !== undefined,
103
+ ack: 'draft-02',
104
+ allowFetch: () => false
105
+ },
106
+ resourceIndicators: {
107
+ enabled: true,
108
+ useGrantedResource: () => true,
109
+ /**
110
+ * Real clients omit the RFC 8707 parameter (older Codex logins, Google
111
+ * ADK, Gemini Enterprise), so "none requested" has to mean something.
112
+ *
113
+ * - DEFAULT_RESOURCE, when the operator set one: the token is still
114
+ * bound, just not by the client's choosing.
115
+ * - the issuer itself in unbound mode, which is the pre-0.5 migration
116
+ * behaviour the hand-written server had — one token reaches every
117
+ * route, and the per-route check is what narrows it.
118
+ * - a refusal when binding is required, matching the old
119
+ * `invalid_target` rather than letting the flow die later as
120
+ * `access_denied`, which says nothing the client can act on.
121
+ */
122
+ defaultResource: () => {
123
+ if (options.defaultResource)
124
+ return options.defaultResource.href;
125
+ if (!options.requireResource)
126
+ return options.externalUrl;
127
+ throw new errors.InvalidTarget('A resource indicator is required');
128
+ },
129
+ getResourceServerInfo: (_ctx, resourceIndicator) => {
130
+ // The issuer itself is the unbound audience; it is not an MCP route,
131
+ // so the canonicaliser would refuse it.
132
+ if (resourceIndicator === options.externalUrl) {
133
+ return { audience: resourceIndicator, accessTokenTTL: ACCESS_TOKEN_TTL_S, scope: HUB_SCOPE };
134
+ }
135
+ // Same canonicalisation the resource server applies (/hub/mcp -> /hub,
136
+ // /<name> -> /<name>/mcp). Minting a token for an audience the hub
137
+ // would not accept produces a 401 the client cannot act on.
138
+ const canonical = resolve(resourceIndicator);
139
+ if (!canonical)
140
+ throw new errors.InvalidTarget('unknown resource');
141
+ return {
142
+ audience: canonical.href,
143
+ accessTokenTTL: ACCESS_TOKEN_TTL_S,
144
+ scope: HUB_SCOPE
145
+ // No `accessTokenFormat` -> opaque. Deliberate: oidc-provider never
146
+ // persists a JWT access token (formats/jwt.js returns no payload),
147
+ // so with JWTs `revokedBefore` could only be an expiry window.
148
+ // Opaque tokens go through the adapter, where revocation is immediate.
149
+ };
150
+ }
151
+ }
152
+ },
153
+ /**
154
+ * Exactly the three the hub advertised before, and no more. Dropping
155
+ * `client_secret_basic` and `client_secret_jwt` from oidc-provider's
156
+ * defaults is a deliberate narrowing: nothing the hub issues can use them,
157
+ * and an authentication mechanism nobody uses is only an attack surface.
158
+ */
159
+ clientAuthMethods: ['client_secret_post', 'none', 'private_key_jwt'],
160
+ // The set the hub advertised before. oidc-provider's default is narrower
161
+ // (one algorithm per family), which would refuse a private_key_jwt client
162
+ // that signs with RS384 -- something the old document promised to accept.
163
+ enabledJWA: {
164
+ clientAuthSigningAlgValues: ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512', 'EdDSA']
165
+ },
166
+ // --- Quirk 3: refresh tokens without offline_access ---------------------
167
+ issueRefreshToken: async (_ctx, client) => client.grantTypeAllowed('refresh_token'),
168
+ // Not optional alongside the above: the default marks every code issued
169
+ // without `offline_access` as session-bound, and a session-bound token
170
+ // stops resolving once the browser session is gone. The hub's clients are
171
+ // headless.
172
+ expiresWithSession: async () => false,
173
+ // Forces refresh_token into grant_types for clients that sent their own
174
+ // list. Runs between #assign and #initialize, must be synchronous, and
175
+ // needs at least one declared property or it is never called.
176
+ extraClientMetadata: {
177
+ properties: ['x_mcp_hub'],
178
+ validator(_ctx, key, _value, metadata) {
179
+ if (key !== 'x_mcp_hub')
180
+ return;
181
+ /**
182
+ * Two adjustments, and the second one is why claude.ai could not
183
+ * connect to 0.11.0 at all.
184
+ *
185
+ * A client may declare grant types this server does not offer, and
186
+ * oidc-provider refuses the whole registration when it sees one —
187
+ * `invalid_client_metadata: grant_types can only contain
188
+ * 'authorization_code' or 'refresh_token'`. claude.ai's metadata
189
+ * document declares `urn:ietf:params:oauth:grant-type:jwt-bearer`
190
+ * alongside the two it actually uses here, so every attempt died on a
191
+ * grant it was never going to ask for. The hand-written server this
192
+ * replaced ignored the extras, which is why the failure arrived with
193
+ * the rewrite rather than with the client.
194
+ *
195
+ * Dropping them is the right answer rather than a lenient one: what a
196
+ * client may do is decided by what the authorization server supports,
197
+ * the registration response says which types were actually registered
198
+ * (RFC 7591 §3.2.1), and a client reading that response learns the
199
+ * truth. Rejecting instead makes an optimistic superset — the sensible
200
+ * thing for a client that talks to many servers — fatal.
201
+ */
202
+ const declared = metadata.grant_types ?? ['authorization_code'];
203
+ const grants = declared.filter(grant => SUPPORTED_GRANT_TYPES.includes(grant));
204
+ // Everything it asked for is something this server has never heard of.
205
+ // That is not a superset to trim, it is a client aimed at the wrong
206
+ // kind of server, and saying so beats registering it with no way to
207
+ // get a token.
208
+ if (grants.length === 0) {
209
+ throw new errors.InvalidClientMetadata(`grant_types must include one of: ${SUPPORTED_GRANT_TYPES.join(', ')}`);
210
+ }
211
+ if (!grants.includes('refresh_token'))
212
+ grants.push('refresh_token');
213
+ metadata.grant_types = grants;
214
+ /**
215
+ * An empty `scope` is not a narrow scope, it is a malformed one: RFC
216
+ * 6749 §3.3 defines the value as at least one scope-token, so `''` is
217
+ * outside the grammar and client_schema.js refuses the entire
218
+ * registration with `scope must be a non-empty string if provided`.
219
+ *
220
+ * The MCP Inspector sends exactly that. Its scope input is optional,
221
+ * an untouched field serialises as `''` rather than being left out,
222
+ * and the SDK copies `clientMetadata` into the registration body
223
+ * verbatim — so an empty text box, which is the default state, is
224
+ * enough to make dynamic registration impossible. Any client built on
225
+ * `@modelcontextprotocol/client` that carries a scope field it does
226
+ * not fill has the same shape, and none of them can tell from the
227
+ * error that the field they never touched is the problem.
228
+ *
229
+ * Dropping it is the substitution RFC 7591 §3.2.1 provides for ("MAY
230
+ * reject or replace any of the client's requested metadata values"),
231
+ * and it lands the client exactly where omitting the optional field
232
+ * would have. Nothing is widened by that: `scope` is a CEILING on what
233
+ * a client may ask for, the hub authorizes a client and binds the
234
+ * token to a resource, and neither decision reads this field. A scope
235
+ * that says something is left untouched, and `scopes()` still refuses
236
+ * one this server does not know.
237
+ */
238
+ if (metadata.scope === '')
239
+ delete metadata.scope;
240
+ /**
241
+ * The hub's redirect-URI policy, which oidc-provider does not have:
242
+ * it keeps out javascript:/data:/vbscript: but accepts plain http to
243
+ * any host. That last one matters — the authorization code travels in
244
+ * the clear on the final redirect, and a public client has nothing
245
+ * else to prove itself with.
246
+ *
247
+ * A metadata-document client is held to the stricter half: the MCP
248
+ * specification allows it only https or loopback, never a private-use
249
+ * scheme.
250
+ */
251
+ /**
252
+ * A client name goes straight onto the consent page and into the log.
253
+ * The hub clamps it -- newlines out, length capped -- and oidc-provider
254
+ * does not, so an application could otherwise choose a "name" that took
255
+ * over the page it is being approved on.
256
+ */
257
+ if (metadata.client_name !== undefined) {
258
+ metadata.client_name = clampDisplayName(metadata.client_name);
259
+ }
260
+ // A private-use scheme (com.example.app:/cb) is only valid for a native
261
+ // client as far as oidc-provider is concerned, and native is what such
262
+ // a client actually is. Without this the registration is refused and
263
+ // RFC 8252 clients have no way in.
264
+ const uris = metadata.redirect_uris ?? [];
265
+ if (uris.some(uri => !/^https?:/i.test(uri)))
266
+ metadata.application_type = 'native';
267
+ const allowPrivateUseSchemes = !String(metadata.client_id ?? '').startsWith('https://');
268
+ for (const uri of uris) {
269
+ if (!isSafeRedirectUri(uri, { allowPrivateUseSchemes })) {
270
+ throw new errors.InvalidClientMetadata('redirect_uris must be https, loopback http, or a private-use scheme');
271
+ }
272
+ }
273
+ }
274
+ },
275
+ clientDefaults: {
276
+ grant_types: [...SUPPORTED_GRANT_TYPES],
277
+ token_endpoint_auth_method: 'none',
278
+ // Not cosmetic: the hub's key is Ed25519, so RS256 — which is what
279
+ // oidc-provider defaults a new client to — is not an algorithm this
280
+ // server has a key for, and every registration is refused with
281
+ // "id_token_signed_response_alg must be 'EdDSA' or 'Ed25519'". No
282
+ // id_token is ever issued; the client still has to declare an algorithm
283
+ // the server could sign one with.
284
+ id_token_signed_response_alg: 'EdDSA'
285
+ },
286
+ /**
287
+ * The hub approves a CLIENT, not a set of scopes, so the grant is minted
288
+ * here rather than assembled from a consent screen's checkboxes. This is
289
+ * also what makes an already-approved client skip consent, which is what
290
+ * AuthStore.getApproval() records.
291
+ */
292
+ loadExistingGrant: async (ctx) => {
293
+ const clientId = ctx.oidc.client?.clientId;
294
+ if (!clientId)
295
+ return undefined;
296
+ /**
297
+ * The approval is checked FIRST, before any grant already attached to the
298
+ * session, because approval is per redirect TARGET and not per client.
299
+ * Returning the session's existing grant straight away would mean that
300
+ * approving a client for `http://localhost:1234/callback` silently
301
+ * approved it for `/other` as well — a code sent somewhere the user never
302
+ * agreed to.
303
+ *
304
+ * Minting only for an approved client is also what keeps the consent page
305
+ * reachable at all: approving unconditionally would satisfy the prompt
306
+ * every time, and the page that asks "did you start this?" would never be
307
+ * shown again.
308
+ */
309
+ const redirectUri = String(ctx.oidc.params?.redirect_uri ?? '');
310
+ const approval = store.getApproval(clientId);
311
+ const approved = approval?.redirectUris.some(uri => redirectUriMatches(redirectUri, uri)) ?? false;
312
+ if (!approved && !ctx.oidc.result?.consent)
313
+ return undefined;
314
+ const existing = ctx.oidc.result?.consent?.grantId ?? ctx.oidc.session?.grantIdFor(clientId);
315
+ const grant = existing
316
+ ? ((await ctx.oidc.provider.Grant.find(existing)) ??
317
+ new ctx.oidc.provider.Grant({ clientId, accountId: ctx.oidc.session?.accountId }))
318
+ : new ctx.oidc.provider.Grant({ clientId, accountId: ctx.oidc.session?.accountId });
319
+ /**
320
+ * Whatever scope the request asked for has to end up ON the grant, even
321
+ * though the hub issues nothing for it.
322
+ *
323
+ * The hub advertises `scopes_supported: ["openid"]` and cannot stop:
324
+ * oidc-provider's configuration.js adds it unconditionally. A client that
325
+ * reads the discovery document therefore asks for `openid` — claude.ai
326
+ * does — and a requested scope that the grant does not carry leaves the
327
+ * consent prompt unsatisfied. oidc-provider then sends the browser back
328
+ * to the interaction, the consent page is rendered again, approving it
329
+ * finishes the interaction, the resume finds the same missing scope, and
330
+ * round it goes. What that looks like from the outside is an approve
331
+ * button that reloads the page and does nothing, for ever.
332
+ *
333
+ * Granting it is bookkeeping and nothing more: no id_token is ever
334
+ * issued and `features.userinfo` is off, so `openid` on the grant buys
335
+ * the client no claim it could read. The scope set has already been
336
+ * validated against `scopes_supported` by the time this runs, so there is
337
+ * nothing here to widen.
338
+ *
339
+ * This is also why the grant is now *amended* rather than returned as
340
+ * found: a session that already holds a grant from an earlier flow would
341
+ * otherwise keep whatever scopes it was minted with, and a client that
342
+ * later asks for one more would loop the same way with no way out but
343
+ * clearing cookies.
344
+ */
345
+ const scopes = String(ctx.oidc.params?.scope ?? '')
346
+ .split(' ')
347
+ .filter(Boolean);
348
+ if (scopes.length > 0)
349
+ grant.addOIDCScope(scopes.join(' '));
350
+ const requested = ctx.oidc.params?.resource;
351
+ for (const resource of [requested ?? []].flat()) {
352
+ grant.addResourceScope(String(resource), HUB_SCOPE);
353
+ }
354
+ await grant.save();
355
+ return grant;
356
+ },
357
+ findAccount: (_ctx, sub) => ({ accountId: sub, claims: async () => ({ sub }) }),
358
+ /**
359
+ * Caps how long a client assertion may live.
360
+ *
361
+ * oidc-provider only checks that it has not expired, so a client could sign
362
+ * one valid for a year and anything that captured it would hold a
363
+ * reusable credential for that long. The hub has always required five
364
+ * minutes, which is what RFC 7523 §3 recommends and what keeps a leaked
365
+ * assertion close to worthless.
366
+ */
367
+ assertJwtClientAuthClaimsAndHeader: async (_ctx, claims) => {
368
+ const lifetime = Number(claims.exp) - Number(claims.iat ?? claims.exp);
369
+ if (!Number.isFinite(lifetime) || lifetime > MAX_ASSERTION_LIFETIME_S) {
370
+ throw new errors.InvalidClientAuth('assertion is valid for too long');
371
+ }
372
+ },
373
+ /**
374
+ * JSON, not the library's HTML page.
375
+ *
376
+ * This is reached when an authorization request cannot be redirected back
377
+ * -- an unknown client, a redirect_uri that does not match -- which is
378
+ * precisely when the caller is a program rather than a person. The hub has
379
+ * always answered those with a machine-readable body, and a connector that
380
+ * gets HTML has nothing to log but a wall of markup.
381
+ */
382
+ renderError: (ctx, out) => {
383
+ ctx.type = 'json';
384
+ ctx.body = out;
385
+ },
386
+ interactions: {
387
+ url: (_ctx, interaction) => `${options.interactionPath ?? '/interaction'}/${interaction.uid}/`
388
+ },
389
+ // The hub's own signing key. Supplying it keeps oidc-provider from falling
390
+ // back to the development keys it warns about; with opaque access tokens
391
+ // nothing user-facing is signed with it.
392
+ jwks: { keys: [store.privateKey.export({ format: 'jwk' })] },
393
+ cookies: { keys: [store.cookieSecret] },
394
+ ttl: {
395
+ AccessToken: ACCESS_TOKEN_TTL_S,
396
+ RefreshToken: REFRESH_TOKEN_TTL_S,
397
+ AuthorizationCode: CODE_TTL_S,
398
+ Session: SESSION_TTL_S,
399
+ Interaction: CODE_TTL_S,
400
+ Grant: REFRESH_TOKEN_TTL_S
401
+ },
402
+ /**
403
+ * Every outbound request oidc-provider makes goes through the hub's pinned
404
+ * client. With the metadata-document path closed above, that leaves
405
+ * `jwks_uri` for private_key_jwt clients — and that one needs it most:
406
+ * unlike its own CIMD fetch, oidc-provider requests jwks_uri with neither
407
+ * `redirect: 'manual'` nor a body cap (the default limit is Infinity).
408
+ * `safeFetch` refuses non-https, pins the resolved public address so a
409
+ * second DNS answer cannot redirect it inward, and caps the body.
410
+ */
411
+ ...(options.cimd ? { fetch: options.cimd.safeFetch } : {}),
412
+ // Defence in depth behind `safeFetch`: two of the three defaults are
413
+ // Infinity, and an unbounded fetch is not something to discover later.
414
+ fetchResponseBodyLimits: {
415
+ 'client_id metadata document': 5 * 1024,
416
+ jwks_uri: 64 * 1024,
417
+ sector_identifier_uri: 8 * 1024
418
+ }
419
+ };
420
+ const provider = new Provider(issuer, configuration);
421
+ /**
422
+ * RFC 8252 §7.3: a native client listens on an ephemeral loopback port and
423
+ * cannot know it in advance, so `http://127.0.0.1:1234/cb` and
424
+ * `http://127.0.0.1:51234/cb` are the same redirect target. oidc-provider
425
+ * compares exactly and would refuse every run after the first.
426
+ *
427
+ * Uses the same matcher the approval check does, so "approved for this
428
+ * target" and "allowed to redirect there" cannot drift apart.
429
+ */
430
+ const exactlyAllowed = provider.Client.prototype.redirectUriAllowed;
431
+ provider.Client.prototype.redirectUriAllowed = function redirectUriAllowed(value) {
432
+ if (exactlyAllowed.call(this, value))
433
+ return true;
434
+ return (this.redirectUris ?? []).some(uri => redirectUriMatches(value, uri));
435
+ };
436
+ installThrowawaySecret(provider, store);
437
+ installDiscoveryFixups(provider);
438
+ return provider;
439
+ }
440
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../src/auth/oidc/provider.ts"],"names":[],"mappings":"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"}