arc-1 0.9.18 → 0.9.19
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/README.md +26 -26
- package/dist/adt/config.d.ts +1 -1
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/http.d.ts +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js.map +1 -1
- package/dist/authz/policy.d.ts +6 -0
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +20 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/cli.js +21 -3
- package/dist/cli.js.map +1 -1
- package/dist/handlers/dispatch.d.ts +3 -0
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +71 -53
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/schemas.d.ts +4 -4
- package/dist/plugins/manifest-interpreter.d.ts +25 -0
- package/dist/plugins/manifest-interpreter.d.ts.map +1 -0
- package/dist/plugins/manifest-interpreter.js +124 -0
- package/dist/plugins/manifest-interpreter.js.map +1 -0
- package/dist/public/define-tool.d.ts +9 -0
- package/dist/public/define-tool.d.ts.map +1 -0
- package/dist/public/define-tool.js +25 -0
- package/dist/public/define-tool.js.map +1 -0
- package/dist/public/index.d.ts +9 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +10 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/testing.d.ts +26 -0
- package/dist/public/testing.d.ts.map +1 -0
- package/dist/public/testing.js +39 -0
- package/dist/public/testing.js.map +1 -0
- package/dist/public/types.d.ts +87 -0
- package/dist/public/types.d.ts.map +1 -0
- package/dist/public/types.js +4 -0
- package/dist/public/types.js.map +1 -0
- package/dist/registry/tool-registry.d.ts +74 -0
- package/dist/registry/tool-registry.d.ts.map +1 -0
- package/dist/registry/tool-registry.js +59 -0
- package/dist/registry/tool-registry.js.map +1 -0
- package/dist/server/app-url.d.ts +31 -0
- package/dist/server/app-url.d.ts.map +1 -0
- package/dist/server/app-url.js +50 -0
- package/dist/server/app-url.js.map +1 -0
- package/dist/server/audit.d.ts +4 -0
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +19 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts +15 -46
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +105 -375
- package/dist/server/http.js.map +1 -1
- package/dist/server/logger.d.ts +22 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +22 -0
- package/dist/server/logger.js.map +1 -1
- package/dist/server/plugin-loader.d.ts +19 -0
- package/dist/server/plugin-loader.d.ts.map +1 -0
- package/dist/server/plugin-loader.js +162 -0
- package/dist/server/plugin-loader.js.map +1 -0
- package/dist/server/safe-http-client.d.ts +38 -0
- package/dist/server/safe-http-client.d.ts.map +1 -0
- package/dist/server/safe-http-client.js +129 -0
- package/dist/server/safe-http-client.js.map +1 -0
- package/dist/server/server.d.ts +2 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +36 -7
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +8 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -1
- package/package.json +24 -8
- package/dist/adt/btp.d.ts +0 -140
- package/dist/adt/btp.d.ts.map +0 -1
- package/dist/adt/btp.js +0 -427
- package/dist/adt/btp.js.map +0 -1
- package/dist/server/oauth-state.d.ts +0 -92
- package/dist/server/oauth-state.d.ts.map +0 -1
- package/dist/server/oauth-state.js +0 -163
- package/dist/server/oauth-state.js.map +0 -1
- package/dist/server/stateless-client-store.d.ts +0 -173
- package/dist/server/stateless-client-store.d.ts.map +0 -1
- package/dist/server/stateless-client-store.js +0 -503
- package/dist/server/stateless-client-store.js.map +0 -1
- package/dist/server/xsuaa.d.ts +0 -188
- package/dist/server/xsuaa.d.ts.map +0 -1
- package/dist/server/xsuaa.js +0 -464
- package/dist/server/xsuaa.js.map +0 -1
package/dist/server/http.d.ts
CHANGED
|
@@ -24,45 +24,10 @@
|
|
|
24
24
|
*
|
|
25
25
|
* 4. Health endpoint is always unauthenticated — needed for CF health checks.
|
|
26
26
|
*/
|
|
27
|
+
import type { XsuaaCredentials } from '@arc-mcp/xsuaa-auth';
|
|
27
28
|
import type { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
|
28
|
-
import type { Request, Response } from 'express';
|
|
29
29
|
import express from 'express';
|
|
30
|
-
import type { OAuthStateCodec } from './oauth-state.js';
|
|
31
|
-
import type { StatelessDcrClientStore } from './stateless-client-store.js';
|
|
32
30
|
import type { ServerConfig } from './types.js';
|
|
33
|
-
import type { XsuaaCredentials } from './xsuaa.js';
|
|
34
|
-
/**
|
|
35
|
-
* Express handler for ARC-1's `/oauth/callback`, the second half of the
|
|
36
|
-
* XSUAA callback proxy that fixes the `+`-in-state bug (issue #214).
|
|
37
|
-
*
|
|
38
|
-
* XSUAA redirects here (not to the client) with an opaque base64url `state`
|
|
39
|
-
* token that ARC-1's `authorize()` minted. We verify + decode it to recover
|
|
40
|
-
* the client's ORIGINAL `redirect_uri` and `state`, then 302 to the client
|
|
41
|
-
* re-emitting the state via `URL.searchParams` — whose serializer encodes a
|
|
42
|
-
* literal `+` as `%2B`, exactly the encoding the client's parser expects.
|
|
43
|
-
*
|
|
44
|
-
* Removal condition + upstream tracking (XSUAA root cause, arc-1#214,
|
|
45
|
-
* vscode#314715) are documented at the top of `oauth-state.ts`.
|
|
46
|
-
*
|
|
47
|
-
* SECURITY (authorization-code interception, security audit 2026-06): the
|
|
48
|
-
* signed state carries the originating DCR `client_id` (`decoded.clientId`).
|
|
49
|
-
* Before forwarding the auth code (or an error) to `decoded.clientRedirectUri`,
|
|
50
|
-
* we verify that redirect_uri is actually registered for that client. The
|
|
51
|
-
* signature alone is insufficient: all DCR clients share one XSUAA app, so a
|
|
52
|
-
* forged-state attack is blocked by the HMAC, but the redirect target must
|
|
53
|
-
* still belong to the client that will exchange the code. For stateless DCR
|
|
54
|
-
* clients (`arc1-…`) the registered redirect_uris are baked immutably into the
|
|
55
|
-
* signed `client_id`, so this check deterministically rejects an attacker who
|
|
56
|
-
* substitutes their own redirect_uri on a victim's `client_id`. For the shared
|
|
57
|
-
* pre-registered XSUAA default client the redirect_uri is checked against the
|
|
58
|
-
* static allowlist (mirrors xs-security.json) instead — `clientStore` makes
|
|
59
|
-
* both decisions via `checkRedirectUri`.
|
|
60
|
-
*
|
|
61
|
-
* Exported for unit tests; mounted in `startHttpServer`. When `clientStore` is
|
|
62
|
-
* omitted (legacy unit tests of the issue-#214 round-trip) the binding check is
|
|
63
|
-
* skipped; production always passes it.
|
|
64
|
-
*/
|
|
65
|
-
export declare function createOAuthCallbackHandler(stateCodec: OAuthStateCodec, clientStore?: StatelessDcrClientStore): (req: Request, res: Response) => Promise<void>;
|
|
66
31
|
/**
|
|
67
32
|
* Apply security headers (helmet) and opt-in CORS to an Express app.
|
|
68
33
|
*
|
|
@@ -93,16 +58,20 @@ export declare function applySecurityMiddleware(app: express.Application, allowe
|
|
|
93
58
|
export declare function startHttpServer(serverFactory: () => McpServer, config: ServerConfig, xsuaaCredentials?: XsuaaCredentials): Promise<void>;
|
|
94
59
|
/**
|
|
95
60
|
* Create a token verifier for standard auth mode (API key + OIDC).
|
|
96
|
-
* Returns AuthInfo so the MCP SDK populates extra.authInfo on the request,
|
|
97
|
-
* enabling scope enforcement, per-request safety, and principal propagation.
|
|
98
|
-
*/
|
|
99
|
-
export declare function createStandardVerifier(config: ServerConfig): (token: string) => Promise<import('@modelcontextprotocol/sdk/server/auth/types.js').AuthInfo>;
|
|
100
|
-
/**
|
|
101
|
-
* Extract scopes from an OIDC JWT payload.
|
|
102
61
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
62
|
+
* Built entirely from the `@arc-mcp/xsuaa-auth` package's chained verifier
|
|
63
|
+
* (constant-time api-key compare + hardened OIDC), mirroring how XSUAA mode wires
|
|
64
|
+
* its chain — minus the XSUAA verifier, which standard mode doesn't have. The
|
|
65
|
+
* chain order is XSUAA → OIDC → api-key; with no XSUAA verifier that collapses to
|
|
66
|
+
* OIDC → api-key, which is order-immaterial here (token types are disjoint) and
|
|
67
|
+
* preserves the previous behavior (api-key matched first only mattered because
|
|
68
|
+
* the two paths never overlap). Returns `AuthInfo` so the MCP SDK populates
|
|
69
|
+
* `extra.authInfo` on the request context, enabling scope enforcement,
|
|
70
|
+
* per-request safety, and principal propagation.
|
|
71
|
+
*
|
|
72
|
+
* Async because the package is dynamic-imported (lazy, consistent with the rest
|
|
73
|
+
* of this module); the returned verifier itself is the package's sync-built
|
|
74
|
+
* chained verifier.
|
|
106
75
|
*/
|
|
107
|
-
export declare function
|
|
76
|
+
export declare function createStandardVerifier(config: ServerConfig): Promise<import('@arc-mcp/xsuaa-auth').Verifier>;
|
|
108
77
|
//# sourceMappingURL=http.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAC;AAIrF,OAAO,OAAO,MAAM,SAAS,CAAC;AAM9B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA2B/C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI,CA6DhG;AAuCD;;GAEG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,MAAM,SAAS,EAC9B,MAAM,EAAE,YAAY,EACpB,gBAAgB,CAAC,EAAE,gBAAgB,GAClC,OAAO,CAAC,IAAI,CAAC,CAgZf;AAkDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,qBAAqB,EAAE,QAAQ,CAAC,CAOlH"}
|
package/dist/server/http.js
CHANGED
|
@@ -30,237 +30,31 @@ import express from 'express';
|
|
|
30
30
|
import helmet from 'helmet';
|
|
31
31
|
import { expandScopes } from '../authz/policy.js';
|
|
32
32
|
import { API_KEY_PROFILES } from './config.js';
|
|
33
|
-
import { logger } from './logger.js';
|
|
33
|
+
import { authLibLogger, logger } from './logger.js';
|
|
34
34
|
import { VERSION } from './server.js';
|
|
35
|
-
// ───
|
|
35
|
+
// ─── API Key Entry Mapping ───────────────────────────────────────────
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* `
|
|
37
|
+
* Map ARC-1's `config.apiKeys` (`{key, profile}[]`) to the package's
|
|
38
|
+
* `ApiKeyEntry[]` (`{key, scopes, clientId}`) so the package's constant-time
|
|
39
|
+
* api-key verifier resolves them identically to ARC-1's previous profile-based
|
|
40
|
+
* matching:
|
|
41
|
+
* - scopes come from `API_KEY_PROFILES[profile]` after `expandScopes`,
|
|
42
|
+
* - clientId is `api-key:<profile>` (same audit/identity string as before).
|
|
43
|
+
* Entries whose profile is unknown are dropped (defense in depth — profiles are
|
|
44
|
+
* already validated at config-parse time). Used by BOTH XSUAA and standard mode.
|
|
39
45
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* Is this a loopback HTTP redirect URI (`http://localhost|127.0.0.1|[::1]`)?
|
|
50
|
-
* Such callbacks are ephemeral local listeners that native MCP clients (GitHub
|
|
51
|
-
* Copilot, MCP Inspector) tear down on failure — so on an OAuth error we render
|
|
52
|
-
* a self-hosted page for them rather than 302-ing to a dead port. Hosted HTTPS
|
|
53
|
-
* callbacks (claude.ai, Copilot Studio) and custom-scheme app callbacks
|
|
54
|
-
* (`vscode:`, `cursor:`) are live and expect the spec error redirect, so they
|
|
55
|
-
* keep getting it.
|
|
56
|
-
*/
|
|
57
|
-
function isLoopbackHttpRedirect(url) {
|
|
58
|
-
if (url.protocol !== 'http:')
|
|
59
|
-
return false;
|
|
60
|
-
const host = url.hostname.toLowerCase();
|
|
61
|
-
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Render a self-hosted OAuth error page for `/oauth/callback`. Surfaces the
|
|
65
|
-
* IdP's error to the human (loopback MCP clients usually can't — they close
|
|
66
|
-
* their listener on failure) with an actionable hint for the most common case,
|
|
67
|
-
* `invalid_scope` (authenticated but no granted scopes → an admin must assign an
|
|
68
|
-
* ARC-1 role collection under the user's login IdP). `clientReturnUrl` carries
|
|
69
|
-
* the error + original state for the rare client still listening.
|
|
70
|
-
*/
|
|
71
|
-
function renderOAuthErrorPage(error, errorDescription, clientReturnUrl) {
|
|
72
|
-
const hint = error === 'invalid_scope'
|
|
73
|
-
? 'You are signed in, but your user is not granted any ARC-1 scopes. An administrator must assign you an ARC-1 role collection (for example "ARC-1 Admin") under the identity provider you sign in with — see the ARC-1 authorization docs.'
|
|
74
|
-
: 'Retry the sign-in from your MCP client. If it keeps failing, share this error with your ARC-1 administrator.';
|
|
75
|
-
const descBlock = errorDescription ? `<p><code>${escapeHtml(errorDescription)}</code></p>` : '';
|
|
76
|
-
return ('<!doctype html><html><head><meta charset="utf-8"><title>ARC-1 sign-in failed</title></head>' +
|
|
77
|
-
'<body style="font-family:sans-serif;max-width:42rem;margin:3rem auto;padding:0 1rem;line-height:1.5">' +
|
|
78
|
-
'<h1>ARC-1 sign-in failed</h1>' +
|
|
79
|
-
`<p><strong>Error:</strong> <code>${escapeHtml(error)}</code></p>` +
|
|
80
|
-
descBlock +
|
|
81
|
-
`<p>${escapeHtml(hint)}</p>` +
|
|
82
|
-
`<p><a href="${escapeHtml(clientReturnUrl)}">Return to your application</a></p>` +
|
|
83
|
-
'</body></html>');
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Express handler for ARC-1's `/oauth/callback`, the second half of the
|
|
87
|
-
* XSUAA callback proxy that fixes the `+`-in-state bug (issue #214).
|
|
88
|
-
*
|
|
89
|
-
* XSUAA redirects here (not to the client) with an opaque base64url `state`
|
|
90
|
-
* token that ARC-1's `authorize()` minted. We verify + decode it to recover
|
|
91
|
-
* the client's ORIGINAL `redirect_uri` and `state`, then 302 to the client
|
|
92
|
-
* re-emitting the state via `URL.searchParams` — whose serializer encodes a
|
|
93
|
-
* literal `+` as `%2B`, exactly the encoding the client's parser expects.
|
|
94
|
-
*
|
|
95
|
-
* Removal condition + upstream tracking (XSUAA root cause, arc-1#214,
|
|
96
|
-
* vscode#314715) are documented at the top of `oauth-state.ts`.
|
|
97
|
-
*
|
|
98
|
-
* SECURITY (authorization-code interception, security audit 2026-06): the
|
|
99
|
-
* signed state carries the originating DCR `client_id` (`decoded.clientId`).
|
|
100
|
-
* Before forwarding the auth code (or an error) to `decoded.clientRedirectUri`,
|
|
101
|
-
* we verify that redirect_uri is actually registered for that client. The
|
|
102
|
-
* signature alone is insufficient: all DCR clients share one XSUAA app, so a
|
|
103
|
-
* forged-state attack is blocked by the HMAC, but the redirect target must
|
|
104
|
-
* still belong to the client that will exchange the code. For stateless DCR
|
|
105
|
-
* clients (`arc1-…`) the registered redirect_uris are baked immutably into the
|
|
106
|
-
* signed `client_id`, so this check deterministically rejects an attacker who
|
|
107
|
-
* substitutes their own redirect_uri on a victim's `client_id`. For the shared
|
|
108
|
-
* pre-registered XSUAA default client the redirect_uri is checked against the
|
|
109
|
-
* static allowlist (mirrors xs-security.json) instead — `clientStore` makes
|
|
110
|
-
* both decisions via `checkRedirectUri`.
|
|
111
|
-
*
|
|
112
|
-
* Exported for unit tests; mounted in `startHttpServer`. When `clientStore` is
|
|
113
|
-
* omitted (legacy unit tests of the issue-#214 round-trip) the binding check is
|
|
114
|
-
* skipped; production always passes it.
|
|
115
|
-
*/
|
|
116
|
-
export function createOAuthCallbackHandler(stateCodec, clientStore) {
|
|
117
|
-
return async (req, res) => {
|
|
118
|
-
const stateToken = typeof req.query.state === 'string' ? req.query.state : '';
|
|
119
|
-
const decoded = stateCodec.decode(stateToken);
|
|
120
|
-
if (decoded.kind !== 'ok') {
|
|
121
|
-
logger.warn('OAuth callback: invalid state token', { reason: decoded.reason });
|
|
122
|
-
// We cannot safely redirect anywhere — the client redirect_uri lives
|
|
123
|
-
// inside the (unverified) token. Return a terminal error page.
|
|
124
|
-
res
|
|
125
|
-
.status(400)
|
|
126
|
-
.type('html')
|
|
127
|
-
.send('<!doctype html><html><body style="font-family:sans-serif;padding:2rem">' +
|
|
128
|
-
'<h1>Authentication failed</h1>' +
|
|
129
|
-
'<p>The OAuth state token was invalid or expired. Please retry the sign-in from your MCP client.</p>' +
|
|
130
|
-
'</body></html>');
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// ── Client-binding validation (authorization-code interception defense) ──
|
|
134
|
-
// Verify the recovered redirect_uri is an allowed target for the client_id
|
|
135
|
-
// that minted this state, BEFORE the success or error branches below — so
|
|
136
|
-
// neither a code nor an error response is ever steered to an unverified URI.
|
|
137
|
-
// The store decides per client type: a DCR client (`arc1-…`) is checked
|
|
138
|
-
// against the redirect_uris baked into its signed id; the shared XSUAA
|
|
139
|
-
// default client is checked against the static allowlist (mirrors
|
|
140
|
-
// xs-security.json), statelessly. Fails CLOSED on any lookup error.
|
|
141
|
-
if (clientStore && decoded.clientId) {
|
|
142
|
-
let verdict;
|
|
143
|
-
try {
|
|
144
|
-
verdict = await clientStore.checkRedirectUri(decoded.clientId, decoded.clientRedirectUri);
|
|
145
|
-
}
|
|
146
|
-
catch (err) {
|
|
147
|
-
logger.warn('OAuth callback: redirect_uri check threw — failing closed', {
|
|
148
|
-
clientId: decoded.clientId,
|
|
149
|
-
error: err instanceof Error ? err.message : String(err),
|
|
150
|
-
});
|
|
151
|
-
verdict = 'unknown_client';
|
|
152
|
-
}
|
|
153
|
-
if (verdict === 'unknown_client') {
|
|
154
|
-
logger.warn('OAuth callback: state references unknown client_id', { clientId: decoded.clientId });
|
|
155
|
-
res
|
|
156
|
-
.status(400)
|
|
157
|
-
.type('html')
|
|
158
|
-
.send('<!doctype html><html><body style="font-family:sans-serif;padding:2rem">' +
|
|
159
|
-
'<h1>Authentication failed</h1>' +
|
|
160
|
-
'<p>The OAuth client referenced in the state token is no longer valid. Please retry the sign-in.</p>' +
|
|
161
|
-
'</body></html>');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (verdict === 'unregistered') {
|
|
165
|
-
logger.warn('OAuth callback: redirect_uri not allowed for client', {
|
|
166
|
-
clientId: decoded.clientId,
|
|
167
|
-
redirectUri: decoded.clientRedirectUri,
|
|
168
|
-
});
|
|
169
|
-
res
|
|
170
|
-
.status(400)
|
|
171
|
-
.type('html')
|
|
172
|
-
.send('<!doctype html><html><body style="font-family:sans-serif;padding:2rem">' +
|
|
173
|
-
'<h1>Authentication failed</h1>' +
|
|
174
|
-
'<p>The redirect URI in the state token is not registered for this client. Please retry the sign-in.</p>' +
|
|
175
|
-
'</body></html>');
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
let target;
|
|
180
|
-
try {
|
|
181
|
-
target = new URL(decoded.clientRedirectUri);
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
logger.warn('OAuth callback: stored redirect_uri is not a valid URL');
|
|
185
|
-
res.status(400).type('html').send('<!doctype html><html><body>Invalid redirect target.</body></html>');
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
// On error there is no auth code. Forward the error to the client per the
|
|
189
|
-
// OAuth spec — EXCEPT for loopback HTTP callbacks. Native MCP clients
|
|
190
|
-
// (GitHub Copilot, MCP Inspector, …) tear down their ephemeral localhost
|
|
191
|
-
// listener the instant the flow fails, so a 302 there lands on a dead port
|
|
192
|
-
// and the user sees a blank ERR_CONNECTION_REFUSED with no clue why. For
|
|
193
|
-
// those we render a self-hosted page that surfaces the real reason (e.g.
|
|
194
|
-
// invalid_scope → missing role collection), with a best-effort link back.
|
|
195
|
-
// Hosted HTTPS callbacks (claude.ai, Copilot Studio) and custom-scheme app
|
|
196
|
-
// callbacks (vscode:, cursor:) are live and expect the redirect, so they
|
|
197
|
-
// keep getting it.
|
|
198
|
-
const error = typeof req.query.error === 'string' ? req.query.error : undefined;
|
|
199
|
-
if (error) {
|
|
200
|
-
const errorDescription = typeof req.query.error_description === 'string' ? req.query.error_description : '';
|
|
201
|
-
if (decoded.clientState !== undefined)
|
|
202
|
-
target.searchParams.set('state', decoded.clientState);
|
|
203
|
-
target.searchParams.set('error', error);
|
|
204
|
-
if (errorDescription)
|
|
205
|
-
target.searchParams.set('error_description', errorDescription);
|
|
206
|
-
const loopback = isLoopbackHttpRedirect(target);
|
|
207
|
-
logger.warn('OAuth callback: identity provider returned an error', {
|
|
208
|
-
error,
|
|
209
|
-
errorDescriptionPreview: errorDescription.slice(0, 200),
|
|
210
|
-
clientRedirectUriHost: target.host,
|
|
211
|
-
loopback,
|
|
212
|
-
});
|
|
213
|
-
if (loopback) {
|
|
214
|
-
res
|
|
215
|
-
.status(400)
|
|
216
|
-
.type('html')
|
|
217
|
-
.send(renderOAuthErrorPage(error, errorDescription, target.toString()));
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
res.redirect(302, target.toString());
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
// Success: forward the authorization code, re-attaching the client's
|
|
225
|
-
// ORIGINAL state. URLSearchParams serialization encodes `+` as `%2B`, which
|
|
226
|
-
// is exactly what fixes the round-trip (issue #214).
|
|
227
|
-
const code = typeof req.query.code === 'string' ? req.query.code : '';
|
|
228
|
-
target.searchParams.set('code', code);
|
|
229
|
-
if (decoded.clientState !== undefined) {
|
|
230
|
-
target.searchParams.set('state', decoded.clientState);
|
|
231
|
-
}
|
|
232
|
-
logger.debug('OAuth callback: redirecting to client', {
|
|
233
|
-
clientRedirectUriHost: target.host,
|
|
234
|
-
hasState: decoded.clientState !== undefined,
|
|
235
|
-
});
|
|
236
|
-
res.redirect(302, target.toString());
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
// ─── API Key Matching Helper ─────────────────────────────────────────
|
|
240
|
-
/**
|
|
241
|
-
* Match a token against configured API keys (multi-key with profiles).
|
|
242
|
-
* Returns the matched entry's profile and scopes, or undefined if no match.
|
|
243
|
-
*/
|
|
244
|
-
function matchApiKey(token, config) {
|
|
245
|
-
// Multi-key: each API key has a named profile that maps to a scope set + partial SafetyConfig
|
|
246
|
-
if (config.apiKeys) {
|
|
247
|
-
for (const entry of config.apiKeys) {
|
|
248
|
-
if (token === entry.key) {
|
|
249
|
-
const profile = API_KEY_PROFILES[entry.profile];
|
|
250
|
-
if (!profile) {
|
|
251
|
-
// Should have been caught at config parse; defense in depth
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
const scopes = expandScopes(profile.scopes);
|
|
255
|
-
return { profile: entry.profile, scopes, clientId: `api-key:${entry.profile}` };
|
|
256
|
-
}
|
|
257
|
-
}
|
|
46
|
+
function toApiKeyEntries(config) {
|
|
47
|
+
if (!config.apiKeys)
|
|
48
|
+
return [];
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (const entry of config.apiKeys) {
|
|
51
|
+
const profile = API_KEY_PROFILES[entry.profile];
|
|
52
|
+
if (!profile)
|
|
53
|
+
continue;
|
|
54
|
+
entries.push({ key: entry.key, scopes: expandScopes(profile.scopes), clientId: `api-key:${entry.profile}` });
|
|
258
55
|
}
|
|
259
|
-
return
|
|
56
|
+
return entries;
|
|
260
57
|
}
|
|
261
|
-
// ─── JWKS / JWT types (lazy-loaded from jose) ────────────────────────
|
|
262
|
-
let joseModule = null;
|
|
263
|
-
let jwksClient = null;
|
|
264
58
|
// ─── Security Middleware (helmet + opt-in CORS) ──────────────────────
|
|
265
59
|
/**
|
|
266
60
|
* Apply security headers (helmet) and opt-in CORS to an Express app.
|
|
@@ -437,8 +231,8 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials) {
|
|
|
437
231
|
if (config.xsuaaAuth && xsuaaCredentials) {
|
|
438
232
|
const { mcpAuthRouter } = await import('@modelcontextprotocol/sdk/server/auth/router.js');
|
|
439
233
|
const { requireBearerAuth } = await import('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js');
|
|
440
|
-
const { createXsuaaOAuthProvider, createChainedTokenVerifier, createXsuaaTokenVerifier } = await import('
|
|
441
|
-
const { getAppUrl } = await import('
|
|
234
|
+
const { createXsuaaOAuthProvider, createChainedTokenVerifier, createXsuaaTokenVerifier, createOidcVerifier, createOAuthCallbackHandler, } = await import('@arc-mcp/xsuaa-auth');
|
|
235
|
+
const { getAppUrl } = await import('./app-url.js');
|
|
442
236
|
// Determine app URL for OAuth metadata
|
|
443
237
|
const appUrl = getAppUrl() ?? `http://${bindHost}:${port}`;
|
|
444
238
|
// Compute the prefix-aware public base once, up front — it's needed both
|
|
@@ -453,15 +247,33 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials) {
|
|
|
453
247
|
// URL sent to XSUAA as redirect_uri; the Express route is mounted at the root
|
|
454
248
|
// `/oauth/callback` below since the proxy strips the prefix before forwarding.
|
|
455
249
|
const oauthCallbackUrl = `${oauthFullBase}/oauth/callback`;
|
|
456
|
-
// Create XSUAA provider + chained verifier
|
|
250
|
+
// Create XSUAA provider + chained verifier.
|
|
251
|
+
//
|
|
252
|
+
// The package defaults its client_id prefix + KDF labels to `mcp-*`. ARC-1
|
|
253
|
+
// MUST override them to the historical `arc1-*` values so client_ids and
|
|
254
|
+
// OAuth-state tokens minted by previous ARC-1 releases keep validating
|
|
255
|
+
// (the signing-key derivation folds in the KDF label, and the prefix gates
|
|
256
|
+
// `getClient`). These three strings are part of ARC-1's on-the-wire
|
|
257
|
+
// contract — do not change them.
|
|
457
258
|
const { provider, clientStore, stateCodec } = createXsuaaOAuthProvider(xsuaaCredentials, appUrl, {
|
|
259
|
+
clientIdPrefix: 'arc1-',
|
|
260
|
+
dcrKdfLabel: 'arc1-dcr/v1',
|
|
261
|
+
stateKdfLabel: 'arc1-oauth-state/v1',
|
|
458
262
|
dcrTtlSeconds: config.oauthDcrTtlSeconds,
|
|
459
263
|
dcrSigningSecret: config.dcrSigningSecret,
|
|
460
264
|
callbackUrl: oauthCallbackUrl,
|
|
265
|
+
logger: authLibLogger,
|
|
461
266
|
});
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
267
|
+
// Inject ARC-1's scope-expansion policy + logger so the verifier emits the
|
|
268
|
+
// same expanded AuthInfo.scopes ARC-1 produced before the extraction.
|
|
269
|
+
const xsuaaVerifier = createXsuaaTokenVerifier(xsuaaCredentials, { expandScopes, logger: authLibLogger });
|
|
270
|
+
// OIDC verifier comes from the package (hardened: pinned `algorithms`
|
|
271
|
+
// allowlist, no `sub` in debug logs). `buildPackageOidcVerifier` threads
|
|
272
|
+
// ARC-1's historical contract (accepted scopes, `expandScopes`, the
|
|
273
|
+
// `['read']` fallback, clock tolerance). Construction is SYNC — the package
|
|
274
|
+
// lazy-imports jose + memoizes the JWKS on first verify.
|
|
275
|
+
const oidcVerifier = config.oidcIssuer ? buildPackageOidcVerifier(config, createOidcVerifier) : undefined;
|
|
276
|
+
const chainedVerifier = createChainedTokenVerifier({ apiKeys: toApiKeyEntries(config) }, xsuaaVerifier, oidcVerifier, { expandScopes, logger: authLibLogger });
|
|
465
277
|
// Include resourceMetadataUrl so the 401 WWW-Authenticate header contains the PRM URL.
|
|
466
278
|
// Copilot Studio (and other PRM-aware clients) use this to discover the OAuth endpoints.
|
|
467
279
|
const resourceMetadataUrl = `${appUrl}/.well-known/oauth-protected-resource/mcp`;
|
|
@@ -583,7 +395,7 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials) {
|
|
|
583
395
|
// XSUAA as oauthCallbackUrl. A strip-prefix proxy maps the public path
|
|
584
396
|
// back to this root route. Handler is extracted (exported) so the
|
|
585
397
|
// state-round-trip contract is unit-testable without a live XSUAA.
|
|
586
|
-
app.get('/oauth/callback', createOAuthCallbackHandler(stateCodec, clientStore));
|
|
398
|
+
app.get('/oauth/callback', createOAuthCallbackHandler(stateCodec, clientStore, { logger: authLibLogger }));
|
|
587
399
|
// ─── Path-prefix-aware OAuth metadata override ────────────────
|
|
588
400
|
// The MCP SDK's `mcpAuthRouter` builds endpoint URLs with
|
|
589
401
|
// `new URL("/authorize", baseUrl).href`, which strips any path component
|
|
@@ -668,9 +480,9 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials) {
|
|
|
668
480
|
}
|
|
669
481
|
else {
|
|
670
482
|
// ─── Standard Auth Mode (API key / OIDC) ─────────────────
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
483
|
+
// No JWKS pre-warm needed: the package's OIDC verifier lazy-imports jose and
|
|
484
|
+
// memoizes the remote JWKS on the first verify (and retries on a transient
|
|
485
|
+
// discovery failure instead of caching the rejection).
|
|
674
486
|
// Layer 1 on /mcp also applies outside XSUAA mode — API-key / OIDC / no-auth
|
|
675
487
|
// deployments get the same anonymous-probing protection. OAuth endpoints don't
|
|
676
488
|
// exist in non-XSUAA mode so only /mcp needs mounting here.
|
|
@@ -681,7 +493,7 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials) {
|
|
|
681
493
|
// Use requireBearerAuth so that authInfo is populated on the MCP request context.
|
|
682
494
|
// This enables scope enforcement, per-request safety, and principal propagation.
|
|
683
495
|
const { requireBearerAuth } = await import('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js');
|
|
684
|
-
const verifier = createStandardVerifier(config);
|
|
496
|
+
const verifier = await createStandardVerifier(config);
|
|
685
497
|
const bearerAuth = requireBearerAuth({ verifier: { verifyAccessToken: verifier } });
|
|
686
498
|
app.all('/mcp', bearerAuth, mcpHandler);
|
|
687
499
|
}
|
|
@@ -725,152 +537,70 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials) {
|
|
|
725
537
|
process.exit(1);
|
|
726
538
|
});
|
|
727
539
|
}
|
|
728
|
-
// ───
|
|
540
|
+
// ─── OIDC Verifier (package-backed) ─────────────────────────────────
|
|
729
541
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
542
|
+
* ARC-1's recognised scope names — the `acceptedScopes` allowlist threaded into
|
|
543
|
+
* the package's OIDC verifier so non-ARC-1 claims (e.g. Entra's `User.Read`) are
|
|
544
|
+
* filtered out, exactly as ARC-1's removed inline `extractOidcScopes` did.
|
|
733
545
|
*/
|
|
734
|
-
export function createStandardVerifier(config) {
|
|
735
|
-
return async (token) => {
|
|
736
|
-
// Lazy-import SDK error classes so bearerAuth maps them to 401/403
|
|
737
|
-
const { InvalidTokenError } = await import('@modelcontextprotocol/sdk/server/auth/errors.js');
|
|
738
|
-
// API key: match against multi-key map or single key
|
|
739
|
-
const apiKeyMatch = matchApiKey(token, config);
|
|
740
|
-
if (apiKeyMatch) {
|
|
741
|
-
// expiresAt is required by requireBearerAuth — use far-future expiry for static keys
|
|
742
|
-
const ONE_YEAR_SECS = 365 * 24 * 60 * 60;
|
|
743
|
-
return {
|
|
744
|
-
token,
|
|
745
|
-
clientId: apiKeyMatch.clientId,
|
|
746
|
-
scopes: apiKeyMatch.scopes,
|
|
747
|
-
expiresAt: Math.floor(Date.now() / 1000) + ONE_YEAR_SECS,
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
// OIDC: validate JWT and extract scopes
|
|
751
|
-
if (config.oidcIssuer) {
|
|
752
|
-
try {
|
|
753
|
-
if (!joseModule || !jwksClient) {
|
|
754
|
-
await initJwks(config.oidcIssuer);
|
|
755
|
-
}
|
|
756
|
-
if (!joseModule || !jwksClient) {
|
|
757
|
-
throw new Error('OIDC not initialized — check SAP_OIDC_ISSUER configuration');
|
|
758
|
-
}
|
|
759
|
-
const { payload } = await joseModule.jwtVerify(token, jwksClient, {
|
|
760
|
-
issuer: config.oidcIssuer,
|
|
761
|
-
audience: config.oidcAudience,
|
|
762
|
-
requiredClaims: ['exp'],
|
|
763
|
-
...(config.oidcClockTolerance != null ? { clockTolerance: config.oidcClockTolerance } : {}),
|
|
764
|
-
});
|
|
765
|
-
logger.debug('Standard OIDC JWT validated', { sub: payload.sub, iss: payload.iss });
|
|
766
|
-
const scopes = extractOidcScopes(payload);
|
|
767
|
-
return {
|
|
768
|
-
token,
|
|
769
|
-
clientId: payload.azp ?? payload.sub ?? 'oidc-user',
|
|
770
|
-
scopes,
|
|
771
|
-
expiresAt: payload.exp,
|
|
772
|
-
extra: { sub: payload.sub, iss: payload.iss },
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
catch (err) {
|
|
776
|
-
// Wrap JWT validation errors as InvalidTokenError so bearerAuth returns 401
|
|
777
|
-
if (err instanceof InvalidTokenError)
|
|
778
|
-
throw err;
|
|
779
|
-
throw new InvalidTokenError(err.message ?? 'Invalid token');
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
throw new InvalidTokenError('Authentication failed: invalid token');
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
// ─── OIDC Verifier Factory ───────────────────────────────────────────
|
|
786
|
-
/**
|
|
787
|
-
* Create an Entra ID / OIDC token verifier using jose.
|
|
788
|
-
* Returns a function compatible with the chained verifier.
|
|
789
|
-
*/
|
|
790
|
-
async function createOidcVerifier(config) {
|
|
791
|
-
await initJwks(config.oidcIssuer);
|
|
792
|
-
return async (token) => {
|
|
793
|
-
if (!joseModule || !jwksClient) {
|
|
794
|
-
throw new Error('OIDC not initialized');
|
|
795
|
-
}
|
|
796
|
-
const { payload } = await joseModule.jwtVerify(token, jwksClient, {
|
|
797
|
-
issuer: config.oidcIssuer,
|
|
798
|
-
audience: config.oidcAudience,
|
|
799
|
-
requiredClaims: ['exp'],
|
|
800
|
-
...(config.oidcClockTolerance != null ? { clockTolerance: config.oidcClockTolerance } : {}),
|
|
801
|
-
});
|
|
802
|
-
logger.debug('OIDC JWT validated', { sub: payload.sub, iss: payload.iss });
|
|
803
|
-
const scopes = extractOidcScopes(payload);
|
|
804
|
-
return {
|
|
805
|
-
token,
|
|
806
|
-
clientId: payload.azp ?? payload.sub ?? 'oidc-user',
|
|
807
|
-
scopes,
|
|
808
|
-
expiresAt: payload.exp,
|
|
809
|
-
extra: { sub: payload.sub, iss: payload.iss },
|
|
810
|
-
};
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
// ─── OIDC Scope Extraction ──────────────────────────────────────────
|
|
814
546
|
const KNOWN_SCOPES = ['read', 'write', 'data', 'sql', 'transports', 'git', 'admin'];
|
|
815
547
|
/**
|
|
816
|
-
*
|
|
548
|
+
* Build the package's OIDC verifier from `ServerConfig`, threading ARC-1's
|
|
549
|
+
* historical contract so adopting it changes nothing observable except the two
|
|
550
|
+
* intended hardening wins (pinned `algorithms` allowlist + no `sub` in the debug
|
|
551
|
+
* log — both live inside the package now):
|
|
552
|
+
* - `acceptedScopes = KNOWN_SCOPES` (same filter as the old inline code),
|
|
553
|
+
* - `expandScopes` = ARC-1's policy expander (implied scopes: write⊇read, …),
|
|
554
|
+
* - `fallbackScopes: ['read']` = ARC-1's legacy default — a verified token with
|
|
555
|
+
* no scope claims, OR scope claims none of which are accepted, grants `['read']`
|
|
556
|
+
* (the package defaults this to `[]`; ARC-1 opts back into read-only),
|
|
557
|
+
* - `scopeClaim` left default (`'scope'`, preferred over `scp`),
|
|
558
|
+
* - `clockToleranceSec` only when `config.oidcClockTolerance` is set.
|
|
559
|
+
*
|
|
560
|
+
* `config.oidcAudience` is optional in ARC-1 and was historically forwarded to
|
|
561
|
+
* jose verbatim (jose treats `undefined` as "skip the audience check"); the
|
|
562
|
+
* package forwards it the same way, so we pass it through unchanged. The package
|
|
563
|
+
* signature types `audience` as `string`; the cast preserves the
|
|
564
|
+
* undefined-passthrough without an empty-string audience that would force a
|
|
565
|
+
* (failing) audience check. `config.oidcIssuer` is asserted non-null — every
|
|
566
|
+
* call site is gated on `config.oidcIssuer`.
|
|
817
567
|
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
* when no scope claims are present (safe default for providers that don't emit scopes).
|
|
568
|
+
* Construction is synchronous: the package lazy-imports jose and memoizes the
|
|
569
|
+
* remote JWKS on the first verify.
|
|
821
570
|
*/
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
rawScopes = payload.scp.split(' ').filter((s) => s.length > 0);
|
|
831
|
-
}
|
|
832
|
-
else if (Array.isArray(payload.scp)) {
|
|
833
|
-
rawScopes = payload.scp.filter((s) => typeof s === 'string' && s.length > 0);
|
|
834
|
-
}
|
|
835
|
-
// No scope claims at all → read-only (safe default)
|
|
836
|
-
if (rawScopes === undefined) {
|
|
837
|
-
logger.warn('OIDC JWT has no scope/scp claims — granting read-only access. ' +
|
|
838
|
-
'Configure scope claims in your OIDC provider to grant write/data/sql access.');
|
|
839
|
-
return ['read'];
|
|
840
|
-
}
|
|
841
|
-
// Filter to known scopes
|
|
842
|
-
const filtered = rawScopes.filter((s) => KNOWN_SCOPES.includes(s));
|
|
843
|
-
// If scopes were present but none are known, grant minimum read access
|
|
844
|
-
if (filtered.length === 0) {
|
|
845
|
-
logger.warn('OIDC JWT has scope claims but none match known scopes — granting read-only', { rawScopes });
|
|
846
|
-
return ['read'];
|
|
847
|
-
}
|
|
848
|
-
return expandScopes(filtered);
|
|
571
|
+
function buildPackageOidcVerifier(config, createOidcVerifier) {
|
|
572
|
+
return createOidcVerifier(config.oidcIssuer, config.oidcAudience, {
|
|
573
|
+
acceptedScopes: KNOWN_SCOPES,
|
|
574
|
+
expandScopes,
|
|
575
|
+
fallbackScopes: ['read'],
|
|
576
|
+
...(config.oidcClockTolerance != null ? { clockToleranceSec: config.oidcClockTolerance } : {}),
|
|
577
|
+
logger: authLibLogger,
|
|
578
|
+
});
|
|
849
579
|
}
|
|
580
|
+
// ─── Standard Mode Verifier ─────────────────────────────────────────
|
|
850
581
|
/**
|
|
851
|
-
*
|
|
582
|
+
* Create a token verifier for standard auth mode (API key + OIDC).
|
|
583
|
+
*
|
|
584
|
+
* Built entirely from the `@arc-mcp/xsuaa-auth` package's chained verifier
|
|
585
|
+
* (constant-time api-key compare + hardened OIDC), mirroring how XSUAA mode wires
|
|
586
|
+
* its chain — minus the XSUAA verifier, which standard mode doesn't have. The
|
|
587
|
+
* chain order is XSUAA → OIDC → api-key; with no XSUAA verifier that collapses to
|
|
588
|
+
* OIDC → api-key, which is order-immaterial here (token types are disjoint) and
|
|
589
|
+
* preserves the previous behavior (api-key matched first only mattered because
|
|
590
|
+
* the two paths never overlap). Returns `AuthInfo` so the MCP SDK populates
|
|
591
|
+
* `extra.authInfo` on the request context, enabling scope enforcement,
|
|
592
|
+
* per-request safety, and principal propagation.
|
|
593
|
+
*
|
|
594
|
+
* Async because the package is dynamic-imported (lazy, consistent with the rest
|
|
595
|
+
* of this module); the returned verifier itself is the package's sync-built
|
|
596
|
+
* chained verifier.
|
|
852
597
|
*/
|
|
853
|
-
async function
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const jwksUri = new URL('.well-known/openid-configuration', issuer.endsWith('/') ? issuer : `${issuer}/`);
|
|
861
|
-
const discoveryResp = await fetch(jwksUri.toString());
|
|
862
|
-
const discovery = (await discoveryResp.json());
|
|
863
|
-
if (!discovery.jwks_uri) {
|
|
864
|
-
throw new Error(`No jwks_uri in OIDC discovery response from ${jwksUri}`);
|
|
865
|
-
}
|
|
866
|
-
jwksClient = joseModule.createRemoteJWKSet(new URL(discovery.jwks_uri));
|
|
867
|
-
logger.info('OIDC JWKS initialized', { issuer, jwksUri: discovery.jwks_uri });
|
|
868
|
-
}
|
|
869
|
-
catch (err) {
|
|
870
|
-
logger.error('Failed to initialize OIDC JWKS', {
|
|
871
|
-
issuer,
|
|
872
|
-
error: err instanceof Error ? err.message : String(err),
|
|
873
|
-
});
|
|
874
|
-
}
|
|
598
|
+
export async function createStandardVerifier(config) {
|
|
599
|
+
const { createChainedTokenVerifier, createOidcVerifier } = await import('@arc-mcp/xsuaa-auth');
|
|
600
|
+
const oidcVerifier = config.oidcIssuer ? buildPackageOidcVerifier(config, createOidcVerifier) : undefined;
|
|
601
|
+
return createChainedTokenVerifier({ apiKeys: toApiKeyEntries(config) }, undefined, oidcVerifier, {
|
|
602
|
+
expandScopes,
|
|
603
|
+
logger: authLibLogger,
|
|
604
|
+
});
|
|
875
605
|
}
|
|
876
606
|
//# sourceMappingURL=http.js.map
|