@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.
- package/CHANGELOG.md +619 -0
- package/README.md +189 -64
- package/dist/admin.js +232 -8
- package/dist/admin.js.map +1 -1
- package/dist/auth/address.js +89 -0
- package/dist/auth/address.js.map +1 -0
- package/dist/auth/api-tokens.js +27 -0
- package/dist/auth/api-tokens.js.map +1 -0
- package/dist/auth/cimd.js +367 -0
- package/dist/auth/cimd.js.map +1 -0
- package/dist/auth/consent-page.js +4 -7
- package/dist/auth/consent-page.js.map +1 -1
- package/dist/auth/headers.js +22 -1
- package/dist/auth/headers.js.map +1 -1
- package/dist/auth/login-page.js +4 -7
- package/dist/auth/login-page.js.map +1 -1
- package/dist/auth/oidc/adapter.js +135 -0
- package/dist/auth/oidc/adapter.js.map +1 -0
- package/dist/auth/oidc/interactions.js +187 -0
- package/dist/auth/oidc/interactions.js.map +1 -0
- package/dist/auth/oidc/mount.js +144 -0
- package/dist/auth/oidc/mount.js.map +1 -0
- package/dist/auth/oidc/provider.js +440 -0
- package/dist/auth/oidc/provider.js.map +1 -0
- package/dist/auth/oidc/quirks.js +234 -0
- package/dist/auth/oidc/quirks.js.map +1 -0
- package/dist/auth/oidc/verifier.js +121 -0
- package/dist/auth/oidc/verifier.js.map +1 -0
- package/dist/auth/page.js +22 -0
- package/dist/auth/page.js.map +1 -1
- package/dist/auth/pinned-fetch.js +129 -0
- package/dist/auth/pinned-fetch.js.map +1 -0
- package/dist/auth/protected-resource.js +41 -0
- package/dist/auth/protected-resource.js.map +1 -0
- package/dist/auth/rate-limit.js +156 -0
- package/dist/auth/rate-limit.js.map +1 -0
- package/dist/auth/redirect-uri.js +90 -0
- package/dist/auth/redirect-uri.js.map +1 -0
- package/dist/auth/registration.js +146 -0
- package/dist/auth/registration.js.map +1 -0
- package/dist/auth/session.js +43 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/signed-token.js +49 -0
- package/dist/auth/signed-token.js.map +1 -0
- package/dist/auth/store.js +516 -5
- package/dist/auth/store.js.map +1 -1
- package/dist/auth/text.js +51 -0
- package/dist/auth/text.js.map +1 -0
- package/dist/config.js +184 -5
- package/dist/config.js.map +1 -1
- package/dist/docker-proxy/policy.js +1 -0
- package/dist/docker-proxy/policy.js.map +1 -1
- package/dist/docker-proxy/server.js +1 -0
- package/dist/docker-proxy/server.js.map +1 -1
- package/dist/elicitation.js +0 -0
- package/dist/elicitation.js.map +1 -0
- package/dist/forward.js +0 -0
- package/dist/forward.js.map +1 -0
- package/dist/health.js +22 -3
- package/dist/health.js.map +1 -1
- package/dist/hub.js +337 -29
- package/dist/hub.js.map +1 -1
- package/dist/index.js +218 -23
- package/dist/index.js.map +1 -1
- package/dist/limits.js +13 -1
- package/dist/limits.js.map +1 -1
- package/dist/mcp-limits.js +14 -2
- package/dist/mcp-limits.js.map +1 -1
- package/dist/proxy.js +262 -34
- package/dist/proxy.js.map +1 -1
- package/dist/stdio.js +97 -7
- package/dist/stdio.js.map +1 -1
- package/dist/subscriptions.js +236 -0
- package/dist/subscriptions.js.map +1 -0
- package/dist/supervisor.js +472 -28
- package/dist/supervisor.js.map +1 -1
- package/dist/timings.js +61 -0
- package/dist/timings.js.map +1 -0
- package/dist/tool-filter.js +59 -0
- package/dist/tool-filter.js.map +1 -0
- package/dist/transports/docker.js.map +1 -1
- package/dist/transports/stream.js +33 -20
- package/dist/transports/stream.js.map +1 -1
- package/dist/upstream/auth.js +435 -0
- package/dist/upstream/auth.js.map +1 -0
- package/dist/upstream/login.js +94 -0
- package/dist/upstream/login.js.map +1 -0
- package/dist/upstream/provider.js +286 -0
- package/dist/upstream/provider.js.map +1 -0
- package/dist/upstream/routes.js +97 -0
- package/dist/upstream/routes.js.map +1 -0
- package/package.json +23 -7
- package/dist/auth/provider.js +0 -380
- package/dist/auth/provider.js.map +0 -1
- package/dist/auth/routes.js +0 -223
- package/dist/auth/routes.js.map +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import bcrypt from 'bcryptjs';
|
|
3
|
+
import express, { Router } from 'express';
|
|
4
|
+
import { renderConsentPage } from '../consent-page.js';
|
|
5
|
+
import { allowFormActionTo, authSecurityHeaders } from '../headers.js';
|
|
6
|
+
import { renderLoginPage } from '../login-page.js';
|
|
7
|
+
import { earlyRateLimit, LoginRateLimiter } from '../rate-limit.js';
|
|
8
|
+
import { isLoopbackOnly } from '../redirect-uri.js';
|
|
9
|
+
import { createSessionCookie, csrfToken, readSessionCookie, SESSION_COOKIE, SESSION_TTL_MS, verifyCsrfToken } from '../session.js';
|
|
10
|
+
import { logSafe } from '../text.js';
|
|
11
|
+
import { HUB_ACCOUNT_ID } from './provider.js';
|
|
12
|
+
/**
|
|
13
|
+
* The hub's own login and consent pages, driven by oidc-provider's interaction
|
|
14
|
+
* loop instead of by HubOAuthProvider's hand-rolled one.
|
|
15
|
+
*
|
|
16
|
+
* The pages themselves are unchanged. They post to a RELATIVE `login` and
|
|
17
|
+
* `consent`, and that resolves to `/interaction/<uid>/login` because the page is
|
|
18
|
+
* served from a path ending in a slash — which it has to be anyway, since
|
|
19
|
+
* oidc-provider scopes the interaction cookie to exactly that path. The
|
|
20
|
+
* interaction id still travels in the hidden `request` field too, and is checked
|
|
21
|
+
* so a form cannot be replayed against a different interaction.
|
|
22
|
+
*
|
|
23
|
+
* Two behaviours from the old flow are load-bearing and preserved here:
|
|
24
|
+
*
|
|
25
|
+
* - **Typing the password is the consent.** A first-time client is approved
|
|
26
|
+
* by the login itself, so the consent page is only ever shown to someone
|
|
27
|
+
* who is already signed in and is being asked about a NEW client.
|
|
28
|
+
* - **One session cookie.** `mcp_hub_session` is set alongside
|
|
29
|
+
* oidc-provider's own, because `hasValidSession()` is read outside the auth
|
|
30
|
+
* layer by the upstream OAuth callback. Without it the operator would have
|
|
31
|
+
* to log in twice for two different things.
|
|
32
|
+
*/
|
|
33
|
+
export function createOidcInteractionRoutes(options) {
|
|
34
|
+
const { provider, store } = options;
|
|
35
|
+
const router = Router();
|
|
36
|
+
const rateLimiter = new LoginRateLimiter();
|
|
37
|
+
// The pages this router serves are the ones with a password field on them, so
|
|
38
|
+
// they need the anti-clickjacking and no-store headers at least as much as
|
|
39
|
+
// the token endpoint does. `allowFormActionTo` narrows the CSP afterwards for
|
|
40
|
+
// the single page that has a redirect target to allow.
|
|
41
|
+
router.use(authSecurityHeaders);
|
|
42
|
+
const secure = new URL(options.externalUrl).protocol === 'https:';
|
|
43
|
+
const checkPassword = (password) => {
|
|
44
|
+
if (options.passwordHash)
|
|
45
|
+
return bcrypt.compareSync(password, options.passwordHash);
|
|
46
|
+
const expected = Buffer.from(options.password ?? '');
|
|
47
|
+
const given = Buffer.from(password);
|
|
48
|
+
return expected.length === given.length && crypto.timingSafeEqual(expected, given);
|
|
49
|
+
};
|
|
50
|
+
/** What the page may say about who is asking, and what it must not claim. */
|
|
51
|
+
const identityOf = async (clientId, resource) => {
|
|
52
|
+
const client = await provider.Client.find(clientId);
|
|
53
|
+
const metadata = client?.metadata();
|
|
54
|
+
return {
|
|
55
|
+
clientName: typeof metadata?.client_name === 'string' ? metadata.client_name : undefined,
|
|
56
|
+
resource,
|
|
57
|
+
// A metadata-document client is identified by a URL nobody can forge, so
|
|
58
|
+
// the page can show where the self-declared name came from.
|
|
59
|
+
clientId: options.cimd && clientId.startsWith('https://') ? clientId : undefined,
|
|
60
|
+
loopbackOnly: isLoopbackOnly(metadata?.redirect_uris)
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const expired = (res, status, message) => {
|
|
64
|
+
res.status(status).type('html').send(`<p>${message}</p>`);
|
|
65
|
+
};
|
|
66
|
+
// The two POSTs below carry a limiter and this GET did not, which is the
|
|
67
|
+
// asymmetry CodeQL noticed and it was right. The page is unauthenticated by
|
|
68
|
+
// construction — the uid is all a caller has — and rendering it costs a
|
|
69
|
+
// provider store lookup, so a flood is cheap to send and not free to serve.
|
|
70
|
+
// The same window as its siblings; a person opening a login page a hundred
|
|
71
|
+
// times in fifteen minutes has a different problem.
|
|
72
|
+
router.get('/interaction/:uid', earlyRateLimit(15 * 60_000, 100, 500), async (req, res, next) => {
|
|
73
|
+
try {
|
|
74
|
+
const details = await provider.interactionDetails(req, res);
|
|
75
|
+
const params = details.params;
|
|
76
|
+
const redirectUri = String(params.redirect_uri ?? '');
|
|
77
|
+
const identity = await identityOf(String(params.client_id), params.resource);
|
|
78
|
+
allowFormActionTo(res, redirectUri);
|
|
79
|
+
if (details.prompt.name === 'login') {
|
|
80
|
+
res.status(200).type('html').send(renderLoginPage(details.uid, redirectUri, identity));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const session = readSessionCookie(req.headers.cookie, store.cookieSecret);
|
|
84
|
+
if (!session) {
|
|
85
|
+
expired(res, 401, 'Session expired. Close this window and connect again.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
res
|
|
89
|
+
.status(200)
|
|
90
|
+
.type('html')
|
|
91
|
+
.send(renderConsentPage(details.uid, csrfToken(session, store.cookieSecret), redirectUri, identity));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// interactionDetails throws for an unknown or expired uid, which is not
|
|
95
|
+
// an error worth a stack trace: the window was left open too long.
|
|
96
|
+
expired(res, 400, 'Authorization request expired. Close this window and connect again.');
|
|
97
|
+
void next;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
router.post('/interaction/:uid/login', earlyRateLimit(15 * 60_000, 100, 500), express.urlencoded({ extended: false }), async (req, res) => {
|
|
101
|
+
const ip = req.ip ?? 'unknown';
|
|
102
|
+
const { password, request } = req.body;
|
|
103
|
+
let details;
|
|
104
|
+
try {
|
|
105
|
+
details = await provider.interactionDetails(req, res);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
expired(res, 400, 'Authorization request expired. Close this window and connect again.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (typeof request !== 'string' || request !== details.uid) {
|
|
112
|
+
expired(res, 400, 'Authorization request expired. Close this window and connect again.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (rateLimiter.isBlocked(ip)) {
|
|
116
|
+
console.warn(`mcp-hub: login rate limit exceeded from ${logSafe(ip)}`);
|
|
117
|
+
expired(res, 429, 'Too many attempts. Try again later.');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const params = details.params;
|
|
121
|
+
const redirectUri = String(params.redirect_uri ?? '');
|
|
122
|
+
if (typeof password !== 'string' || !checkPassword(password)) {
|
|
123
|
+
rateLimiter.recordFailure(ip);
|
|
124
|
+
console.warn(`mcp-hub: authentication failure from ${logSafe(ip)}`);
|
|
125
|
+
const identity = await identityOf(String(params.client_id), params.resource);
|
|
126
|
+
allowFormActionTo(res, redirectUri);
|
|
127
|
+
res.status(401).type('html').send(renderLoginPage(details.uid, redirectUri, identity, 'Wrong password'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
rateLimiter.reset(ip);
|
|
131
|
+
console.log(`mcp-hub: successful login from ${logSafe(ip)}`);
|
|
132
|
+
// Typing the password is the consent for the client that triggered it.
|
|
133
|
+
const clientId = String(params.client_id);
|
|
134
|
+
const client = await provider.Client.find(clientId);
|
|
135
|
+
const clientName = client?.metadata()?.client_name;
|
|
136
|
+
store.saveApproval(clientId, redirectUri, typeof clientName === 'string' ? clientName : undefined);
|
|
137
|
+
console.log(`mcp-hub: approved OAuth client ${logSafe(clientId)} for ${logSafe(redirectUri)}`);
|
|
138
|
+
res.cookie(SESSION_COOKIE, createSessionCookie(store.cookieSecret), {
|
|
139
|
+
httpOnly: true,
|
|
140
|
+
secure,
|
|
141
|
+
sameSite: 'lax',
|
|
142
|
+
maxAge: SESSION_TTL_MS,
|
|
143
|
+
path: '/'
|
|
144
|
+
});
|
|
145
|
+
await provider.interactionFinished(req, res, { login: { accountId: HUB_ACCOUNT_ID } }, { mergeWithLastSubmission: false });
|
|
146
|
+
});
|
|
147
|
+
router.post('/interaction/:uid/consent', earlyRateLimit(15 * 60_000, 100, 500), express.urlencoded({ extended: false }), async (req, res) => {
|
|
148
|
+
const { request, csrf, action } = req.body;
|
|
149
|
+
let details;
|
|
150
|
+
try {
|
|
151
|
+
details = await provider.interactionDetails(req, res);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
expired(res, 400, 'Authorization request expired. Close this window and connect again.');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (typeof request !== 'string' || request !== details.uid) {
|
|
158
|
+
expired(res, 400, 'Authorization request expired. Close this window and connect again.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const session = readSessionCookie(req.headers.cookie, store.cookieSecret);
|
|
162
|
+
if (!session) {
|
|
163
|
+
expired(res, 401, 'Session expired. Close this window and connect again.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!verifyCsrfToken(session, csrf, store.cookieSecret)) {
|
|
167
|
+
console.warn(`mcp-hub: consent with an invalid CSRF token from ${logSafe(req.ip ?? 'unknown')}`);
|
|
168
|
+
expired(res, 403, 'This form is no longer valid. Close this window and connect again.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (action !== 'approve') {
|
|
172
|
+
await provider.interactionFinished(req, res, { error: 'access_denied' }, { mergeWithLastSubmission: false });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const params = details.params;
|
|
176
|
+
const clientId = String(params.client_id);
|
|
177
|
+
const client = await provider.Client.find(clientId);
|
|
178
|
+
const clientName = client?.metadata()?.client_name;
|
|
179
|
+
store.saveApproval(clientId, String(params.redirect_uri ?? ''), typeof clientName === 'string' ? clientName : undefined);
|
|
180
|
+
console.log(`mcp-hub: approved OAuth client ${logSafe(clientId)} for ${logSafe(String(params.redirect_uri ?? ''))}`);
|
|
181
|
+
// The grant itself is minted by loadExistingGrant on the resumed request,
|
|
182
|
+
// which is the same code path an already-approved client takes.
|
|
183
|
+
await provider.interactionFinished(req, res, { consent: {} }, { mergeWithLastSubmission: true });
|
|
184
|
+
});
|
|
185
|
+
return router;
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=interactions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactions.js","sourceRoot":"","sources":["../../../src/auth/oidc/interactions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { earlyRateLimit } from '../rate-limit.js';
|
|
2
|
+
import { defaultScope, stripPhantomSecret } from './quirks.js';
|
|
3
|
+
/**
|
|
4
|
+
* Every path the provider answers on, given the `routes` reconfiguration in
|
|
5
|
+
* provider.ts.
|
|
6
|
+
*
|
|
7
|
+
* Two of these are not endpoints and are easy to miss:
|
|
8
|
+
*
|
|
9
|
+
* - `/authorize/:uid` is the RESUME route (initialize_app.js registers it at
|
|
10
|
+
* `${routes.authorization}/:uid`). Without it the redirect back from the
|
|
11
|
+
* login page lands in the hub's 404 handler and the authorization flow
|
|
12
|
+
* dead-ends silently, with a plausible-looking error.
|
|
13
|
+
* - `/register/:id` is RFC 7592 registration management.
|
|
14
|
+
*
|
|
15
|
+
* Both `.well-known` paths are served by the provider with an identical body,
|
|
16
|
+
* which is exactly the alias the hub used to build by hand for ChatGPT.
|
|
17
|
+
*/
|
|
18
|
+
export const OIDC_PATHS = [
|
|
19
|
+
'/authorize',
|
|
20
|
+
'/authorize/:uid',
|
|
21
|
+
'/token',
|
|
22
|
+
'/revoke',
|
|
23
|
+
'/register',
|
|
24
|
+
'/register/:id',
|
|
25
|
+
'/jwks',
|
|
26
|
+
'/.well-known/oauth-authorization-server',
|
|
27
|
+
'/.well-known/openid-configuration'
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Makes EXTERNAL_URL authoritative for every URL the provider publishes.
|
|
31
|
+
*
|
|
32
|
+
* oidc-provider builds endpoint URLs from the REQUEST (`urlFor` resolves against
|
|
33
|
+
* `ctx.href`), not from the issuer. Two consequences the hub cannot accept:
|
|
34
|
+
*
|
|
35
|
+
* - behind a reverse proxy the document would advertise the internal host, and
|
|
36
|
+
* - a request carrying `Host: evil.example` would be answered with a discovery
|
|
37
|
+
* document pointing at evil.example.
|
|
38
|
+
*
|
|
39
|
+
* The hub already treats EXTERNAL_URL as the single source of truth and compares
|
|
40
|
+
* it byte for byte, so overwriting the request's idea of its own origin is the
|
|
41
|
+
* faithful translation rather than a workaround.
|
|
42
|
+
*
|
|
43
|
+
* It also closes the `provider.proxy` gap. That flag is a boolean — it trusts
|
|
44
|
+
* any `X-Forwarded-*` — whereas Express was configured with a CIDR list. Because
|
|
45
|
+
* all three headers are overwritten here, the client cannot influence them, and
|
|
46
|
+
* `X-Forwarded-For` is replaced with the address Express already resolved
|
|
47
|
+
* against TRUSTED_PROXIES.
|
|
48
|
+
*/
|
|
49
|
+
export function forceExternalOrigin(externalUrl) {
|
|
50
|
+
const url = new URL(externalUrl);
|
|
51
|
+
const proto = url.protocol.replace(':', '');
|
|
52
|
+
return (req, _res, next) => {
|
|
53
|
+
req.headers.host = url.host;
|
|
54
|
+
req.headers['x-forwarded-host'] = url.host;
|
|
55
|
+
req.headers['x-forwarded-proto'] = proto;
|
|
56
|
+
if (req.ip)
|
|
57
|
+
req.headers['x-forwarded-for'] = req.ip;
|
|
58
|
+
else
|
|
59
|
+
delete req.headers['x-forwarded-for'];
|
|
60
|
+
next();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The same budgets the hub applied before, per path.
|
|
65
|
+
*
|
|
66
|
+
* They belong here rather than at the call site because the mounted Koa app is
|
|
67
|
+
* never entered by the Express router: a limit that is not put in front of the
|
|
68
|
+
* mount simply does not exist, and nothing about the resulting configuration
|
|
69
|
+
* looks wrong. `/register` is the tightest — registration is open — and it also
|
|
70
|
+
* has to cover `/register/:id`, whose own budget is more generous because
|
|
71
|
+
* routine management would otherwise use up a client's ability to register.
|
|
72
|
+
*/
|
|
73
|
+
export function defaultRateLimits() {
|
|
74
|
+
return {
|
|
75
|
+
'/register': [earlyRateLimit(60 * 60_000, 20, 200)],
|
|
76
|
+
'/register/:id': [earlyRateLimit(60 * 60_000, 60, 600)],
|
|
77
|
+
'/authorize': [earlyRateLimit(15 * 60_000, 100, 1_000)],
|
|
78
|
+
'/authorize/:uid': [earlyRateLimit(15 * 60_000, 100, 1_000)],
|
|
79
|
+
'/token': [earlyRateLimit(15 * 60_000, 50, 500)]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Mounts the authorization server into the hub's Express app.
|
|
84
|
+
*
|
|
85
|
+
* `app.all(path, ...)` and NOT `app.use(path, ...)`: `use` strips the prefix
|
|
86
|
+
* from `req.url`, while oidc-provider derives its own mount path from the
|
|
87
|
+
* issuer (provider.js `#mountPath = new URL(issuer).pathname`). The hub's
|
|
88
|
+
* issuer is the origin root, so stripping would leave the router looking at
|
|
89
|
+
* paths the published URLs do not match.
|
|
90
|
+
*
|
|
91
|
+
* The mounted app is also a dead end — Koa's `handleRequest` always responds,
|
|
92
|
+
* so anything that reaches it never returns to Express. Mounting per path
|
|
93
|
+
* rather than at the root is what keeps `/livez`, `/health` and every MCP route
|
|
94
|
+
* reachable.
|
|
95
|
+
*/
|
|
96
|
+
export function mountOidcProvider(app, provider, store, options) {
|
|
97
|
+
const callback = provider.callback();
|
|
98
|
+
// Safe only in combination with forceExternalOrigin, which overwrites every
|
|
99
|
+
// forwarded header the client could otherwise set.
|
|
100
|
+
provider.proxy = true;
|
|
101
|
+
/**
|
|
102
|
+
* Errors do not cross the boundary. A throw inside the Koa app is caught by
|
|
103
|
+
* `ctx.onerror` and never reaches the hub's four-argument Express error
|
|
104
|
+
* handler, so without this an authorization-server fault would be invisible
|
|
105
|
+
* in the hub's logs.
|
|
106
|
+
*/
|
|
107
|
+
provider.on('server_error', (_ctx, error) => {
|
|
108
|
+
console.error(`mcp-hub: authorization server failed: ${error.message}`);
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* RFC 8414 also defines a path-insertion form, and clients probing a specific
|
|
112
|
+
* resource look up `/.well-known/oauth-authorization-server/<name>/mcp` before
|
|
113
|
+
* falling back to the root document. oidc-provider registers only the exact
|
|
114
|
+
* paths, so without this the suffix form 404s — which the hub answered before.
|
|
115
|
+
* One authorization server covers every resource, so the answer is the same
|
|
116
|
+
* document.
|
|
117
|
+
*/
|
|
118
|
+
for (const base of ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration']) {
|
|
119
|
+
app.get(`${base}/{*splat}`, ...(options.common ?? []), forceExternalOrigin(options.externalUrl), (req, res) => {
|
|
120
|
+
req.url = base;
|
|
121
|
+
callback(req, res);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Built once: each entry holds a counter, and rebuilding per path would give
|
|
125
|
+
// every route a fresh, empty limiter on every iteration.
|
|
126
|
+
const before = options.before ?? defaultRateLimits();
|
|
127
|
+
for (const path of OIDC_PATHS) {
|
|
128
|
+
const handlers = [
|
|
129
|
+
...(options.common ?? []),
|
|
130
|
+
...(before[path] ?? []),
|
|
131
|
+
forceExternalOrigin(options.externalUrl)
|
|
132
|
+
];
|
|
133
|
+
if (path === '/authorize')
|
|
134
|
+
handlers.push(defaultScope);
|
|
135
|
+
// Reads the body, so it has to BE the terminal handler rather than sit in
|
|
136
|
+
// front of one: the request stream can only be consumed once.
|
|
137
|
+
if (path === '/token')
|
|
138
|
+
handlers.push(stripPhantomSecret(store, callback));
|
|
139
|
+
else
|
|
140
|
+
handlers.push((req, res) => callback(req, res));
|
|
141
|
+
app.all(path, ...handlers);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../../../src/auth/oidc/mount.ts"],"names":[],"mappings":"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"}
|