tiny-http-mcp-server 0.1.31 → 0.1.33
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/dist/composition.json +1 -1
- package/node_modules/mcp-oauth/README.md +15 -1
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +1 -6
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +7 -18
- package/node_modules/mcp-oauth/dist/client/client-registration.d.ts +4 -0
- package/node_modules/mcp-oauth/dist/client/client-registration.js +94 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +39 -47
- package/node_modules/mcp-oauth/dist/client/scope.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/scope.js +9 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +4 -1
- package/node_modules/mcp-oauth/dist/client/types.d.ts +23 -4
- package/node_modules/mcp-oauth/dist/index.d.ts +2 -1
- package/node_modules/mcp-oauth/dist/index.js +1 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +23 -4
- package/node_modules/tiny-mcp-client/dist/index.js +160 -61
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -96,7 +96,14 @@ on 401 even when the server omits `error="invalid_token"`. Invalid provenance
|
|
|
96
96
|
fails without quoting token values.
|
|
97
97
|
|
|
98
98
|
Configure `client.metadata.scope` to request a precise scope set; broader
|
|
99
|
-
discovery metadata does not override it.
|
|
99
|
+
discovery metadata does not override it. Explicit scopes must match the cached
|
|
100
|
+
or imported grant's scope set; ordering, repeated spaces and duplicates are
|
|
101
|
+
normalized. An imported grant must declare its scope when a scope is configured.
|
|
102
|
+
Authorization records the requested set when the endpoint omits scope, and
|
|
103
|
+
refresh retains the previous granted set. Mismatched responses never activate
|
|
104
|
+
credentials; an unusable refresh response retains the pending refresh record.
|
|
105
|
+
Select a separate persistence namespace for another scope profile. No scope is
|
|
106
|
+
invented when the client does not configure one.
|
|
100
107
|
|
|
101
108
|
Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
|
|
102
109
|
`tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null` if unknown),
|
|
@@ -105,6 +112,13 @@ Discovery binds an expired or explicitly rejected grant before silent refresh,
|
|
|
105
112
|
using the original configured client. Persisted sessions take precedence,
|
|
106
113
|
including sessions whose tokens have been cleared; an import cannot revive them.
|
|
107
114
|
Input tokens are copied and invalid expiry values fail before authorization.
|
|
115
|
+
Pass a complete DCR response as `client.registration` (or validate untrusted JSON
|
|
116
|
+
with `parseOAuthClientRegistration`). Dynamic clients infer their original ID
|
|
117
|
+
and secret from that response and reuse it without registering another app.
|
|
118
|
+
Explicit ID/secret values must agree with the imported response. Sessions and
|
|
119
|
+
native registration stores retain arrays, issuance/expiry timestamps and JSON
|
|
120
|
+
provider metadata. Registration input is copied, bounded to 64 KiB and 64
|
|
121
|
+
levels, and rejects invalid standard field types and non-JSON extensions.
|
|
108
122
|
Static clients and dynamic initial-grant imports require cached grants to match
|
|
109
123
|
the original normalized client ID and secret. A different client configuration
|
|
110
124
|
fails before attaching or refreshing credentials and retains the stored record;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { type CreateSecretStoreInput } from "auth-store";
|
|
2
|
-
import type { OAuthSessionStore } from "./types.js";
|
|
3
|
-
interface StoredOAuthClient {
|
|
4
|
-
clientId: string;
|
|
5
|
-
clientSecret?: string;
|
|
6
|
-
}
|
|
2
|
+
import type { OAuthSessionStore, StoredOAuthClient } from "./types.js";
|
|
7
3
|
export interface OAuthClientStore {
|
|
8
4
|
load(issuer: string): Promise<StoredOAuthClient | null>;
|
|
9
5
|
save(issuer: string, client: StoredOAuthClient): Promise<void>;
|
|
@@ -12,4 +8,3 @@ export interface OAuthClientStore {
|
|
|
12
8
|
export declare function createAuthStoreSessionStore(options?: CreateSecretStoreInput, namespace?: string): OAuthSessionStore;
|
|
13
9
|
export declare function createAuthStoreClientStore(options: CreateSecretStoreInput, namespace?: string): OAuthClientStore;
|
|
14
10
|
export declare function assertPersistenceNamespace(namespace: string | undefined): void;
|
|
15
|
-
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeStoredOAuthClient } from "./client-registration.js";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { createSecretStore } from "auth-store";
|
|
@@ -62,15 +63,10 @@ export function createAuthStoreClientStore(options, namespace) {
|
|
|
62
63
|
catch {
|
|
63
64
|
throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
|
|
70
|
-
client.clientSecret = getOwnEntry(parsed, "clientSecret");
|
|
71
|
-
}
|
|
72
|
-
return client;
|
|
73
|
-
}
|
|
66
|
+
// Preserve all registration metadata; provider normalization validates its
|
|
67
|
+
// identity and schema before it can authorize a request.
|
|
68
|
+
if (isObjectRecord(parsed) && typeof getOwnEntry(parsed, "clientId") === "string")
|
|
69
|
+
return parsed;
|
|
74
70
|
throw new Error("Stored OAuth client must be a JSON object with clientId");
|
|
75
71
|
},
|
|
76
72
|
async save(issuer, client) {
|
|
@@ -142,20 +138,13 @@ function isStoredOAuthSession(value) {
|
|
|
142
138
|
}
|
|
143
139
|
return (isNonBlankOwnString(value, "resource") &&
|
|
144
140
|
isNonBlankOwnString(value, "authorizationServer") &&
|
|
145
|
-
|
|
141
|
+
normalizeStoredOAuthClient(getOwnEntry(value, "client")) !== null &&
|
|
146
142
|
isStoredOAuthDiscovery(getOwnEntry(value, "discovery")) &&
|
|
143
|
+
(getOwnEntry(value, "requestedScope") === undefined || isNonBlankOwnString(value, "requestedScope")) &&
|
|
147
144
|
(getOwnEntry(value, "refreshState") === undefined ||
|
|
148
145
|
(getOwnEntry(value, "refreshState") === "pending" && getOwnEntry(value, "tokens") === undefined)) &&
|
|
149
146
|
isStoredOAuthTokensOrMissing(getOwnEntry(value, "tokens")));
|
|
150
147
|
}
|
|
151
|
-
function isStoredOAuthClient(value) {
|
|
152
|
-
if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
const clientSecret = getOwnEntry(value, "clientSecret");
|
|
156
|
-
return (clientSecret === undefined ||
|
|
157
|
-
(typeof clientSecret === "string" && clientSecret.trim().length > 0));
|
|
158
|
-
}
|
|
159
148
|
function isStoredOAuthDiscovery(value) {
|
|
160
149
|
if (!isObjectRecord(value)) {
|
|
161
150
|
return false;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OAuthClientRegistration, StoredOAuthClient } from "./types.js";
|
|
2
|
+
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
3
|
+
export declare function parseOAuthClientRegistration(value: unknown): OAuthClientRegistration;
|
|
4
|
+
export declare function normalizeStoredOAuthClient(value: unknown): StoredOAuthClient | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { normalizeOAuthScope } from "./scope.js";
|
|
2
|
+
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
3
|
+
export function parseOAuthClientRegistration(value) {
|
|
4
|
+
const invalid = () => new Error("Invalid OAuth client registration metadata");
|
|
5
|
+
let nodes = 0;
|
|
6
|
+
function copy(input, depth) {
|
|
7
|
+
if (++nodes > 20_000 || depth > 64)
|
|
8
|
+
throw invalid();
|
|
9
|
+
if (input === null || typeof input === "boolean" || typeof input === "string")
|
|
10
|
+
return input;
|
|
11
|
+
if (typeof input === "number" && Number.isFinite(input))
|
|
12
|
+
return input;
|
|
13
|
+
if (typeof input !== "object" || input === null)
|
|
14
|
+
throw invalid();
|
|
15
|
+
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
16
|
+
if (Array.isArray(input)) {
|
|
17
|
+
const length = descriptors.length?.value;
|
|
18
|
+
if (length > 20_000)
|
|
19
|
+
throw invalid();
|
|
20
|
+
const result = [];
|
|
21
|
+
for (let index = 0; index < length; index++) {
|
|
22
|
+
const descriptor = descriptors[String(index)];
|
|
23
|
+
if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
|
|
24
|
+
throw invalid();
|
|
25
|
+
result.push(copy(descriptor.value, depth + 1));
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
|
|
30
|
+
throw invalid();
|
|
31
|
+
return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key, descriptor]) => {
|
|
32
|
+
if (!Object.hasOwn(descriptor, "value"))
|
|
33
|
+
throw invalid();
|
|
34
|
+
return [key, copy(descriptor.value, depth + 1)];
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = copy(value, 0);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw invalid();
|
|
43
|
+
}
|
|
44
|
+
if (typeof result !== "object" || result === null || Array.isArray(result))
|
|
45
|
+
throw invalid();
|
|
46
|
+
const record = result;
|
|
47
|
+
if (!Object.hasOwn(record, "client_id") || typeof record.client_id !== "string" || record.client_id.trim() === "")
|
|
48
|
+
throw new Error("OAuth client registration response missing client_id");
|
|
49
|
+
for (const key of ["client_id", "client_secret", "token_endpoint_auth_method", "application_type", "client_name", "client_uri", "logo_uri", "scope",
|
|
50
|
+
"tos_uri", "policy_uri", "jwks_uri", "software_id", "software_version", "software_statement", "registration_access_token", "registration_client_uri", "issuer"]) {
|
|
51
|
+
if (Object.hasOwn(record, key) && record[key] !== null && typeof record[key] !== "string")
|
|
52
|
+
throw invalid();
|
|
53
|
+
}
|
|
54
|
+
if (typeof record.client_secret === "string" && record.client_secret.trim() === "")
|
|
55
|
+
throw invalid();
|
|
56
|
+
for (const key of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
|
|
57
|
+
const entry = record[key];
|
|
58
|
+
if (Object.hasOwn(record, key) && entry !== null && (!Array.isArray(entry) || entry.some(item => typeof item !== "string")))
|
|
59
|
+
throw invalid();
|
|
60
|
+
}
|
|
61
|
+
for (const key of ["client_id_issued_at", "client_secret_expires_at"]) {
|
|
62
|
+
const entry = record[key];
|
|
63
|
+
if (Object.hasOwn(record, key) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
|
|
64
|
+
throw invalid();
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
normalizeOAuthScope(Object.hasOwn(record, "scope") && record.scope !== null ? record.scope : undefined);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw invalid();
|
|
71
|
+
}
|
|
72
|
+
if (Buffer.byteLength(JSON.stringify(record), "utf8") > 64 * 1024)
|
|
73
|
+
throw invalid();
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
export function normalizeStoredOAuthClient(value) {
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
78
|
+
return null;
|
|
79
|
+
const record = value;
|
|
80
|
+
const clientId = Object.hasOwn(record, "clientId") ? record.clientId : undefined;
|
|
81
|
+
const clientSecret = Object.hasOwn(record, "clientSecret") ? record.clientSecret : undefined;
|
|
82
|
+
if (typeof clientId !== "string" || clientId.trim() === "" ||
|
|
83
|
+
(clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
|
|
84
|
+
return null;
|
|
85
|
+
const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
|
|
86
|
+
if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
|
|
87
|
+
const registration = parseOAuthClientRegistration(record.registration);
|
|
88
|
+
const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : undefined;
|
|
89
|
+
if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
|
|
90
|
+
throw new Error("OAuth client registration does not match the client identity");
|
|
91
|
+
client.registration = registration;
|
|
92
|
+
}
|
|
93
|
+
return client;
|
|
94
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeStoredOAuthClient, parseOAuthClientRegistration } from "./client-registration.js";
|
|
2
|
+
import { normalizeOAuthScope } from "./scope.js";
|
|
1
3
|
import { isIP } from "node:net";
|
|
2
4
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
3
5
|
import { URL } from "node:url";
|
|
@@ -18,6 +20,9 @@ export function createOAuthClientProvider(options) {
|
|
|
18
20
|
export function createDefaultOAuthClientProvider(options) {
|
|
19
21
|
loopbackTarget(options.browser);
|
|
20
22
|
assertPersistenceNamespace(options.persistenceNamespace);
|
|
23
|
+
const clientMetadata = getClientMetadata(options.client);
|
|
24
|
+
const requestedScope = clientMetadata?.scope;
|
|
25
|
+
const configuredClient = normalizeConfiguredClient(options.client);
|
|
21
26
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
|
|
22
27
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
|
|
23
28
|
const now = options.now ?? Date.now;
|
|
@@ -36,11 +41,13 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
36
41
|
const initialGrant = options.initialGrant === undefined ? undefined : {
|
|
37
42
|
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
38
43
|
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
39
|
-
client:
|
|
44
|
+
client: configuredClient
|
|
40
45
|
};
|
|
41
46
|
if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
|
|
42
47
|
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
43
48
|
if (initialGrant?.tokens !== undefined) {
|
|
49
|
+
if (requestedScope !== undefined && initialGrant.tokens.scope !== requestedScope)
|
|
50
|
+
throw new Error("OAuth initial grant does not match the requested OAuth scope");
|
|
44
51
|
try {
|
|
45
52
|
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
46
53
|
}
|
|
@@ -133,7 +140,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
133
140
|
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
134
141
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
135
142
|
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
136
|
-
client: initialGrant.client, tokens: initialGrant.tokens,
|
|
143
|
+
client: initialGrant.client, tokens: initialGrant.tokens,
|
|
144
|
+
...(requestedScope === undefined ? {} : { requestedScope }), discovery: toStoredDiscovery(discovery) };
|
|
137
145
|
await saveSession(canonicalResource, session);
|
|
138
146
|
initialGrantConsumed = true;
|
|
139
147
|
signal?.throwIfAborted();
|
|
@@ -141,11 +149,13 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
141
149
|
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
142
150
|
forceRefresh = false;
|
|
143
151
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
144
|
-
if ((options.client.mode === "static" || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
|
|
145
|
-
const configured =
|
|
152
|
+
if ((options.client.mode === "static" || configuredClient?.registration !== undefined || initialGrant !== undefined) && session !== null && (session.tokens !== undefined || session.refreshState === "pending")) {
|
|
153
|
+
const configured = configuredClient;
|
|
146
154
|
if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
|
|
147
155
|
throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
|
|
148
156
|
}
|
|
157
|
+
if (requestedScope !== undefined && session?.tokens !== undefined && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
|
|
158
|
+
throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
|
|
149
159
|
if (session?.refreshState === "pending") {
|
|
150
160
|
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
|
|
151
161
|
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
@@ -226,10 +236,13 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
226
236
|
...session,
|
|
227
237
|
tokens: {
|
|
228
238
|
...refreshedTokens,
|
|
229
|
-
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
239
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
|
|
240
|
+
scope: refreshedTokens.scope ?? session.tokens.scope
|
|
230
241
|
},
|
|
231
242
|
discovery: toStoredDiscovery(discovery)
|
|
232
243
|
};
|
|
244
|
+
if (requestedScope !== undefined && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
|
|
245
|
+
throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
|
|
233
246
|
await saveSession(resource, updatedSession);
|
|
234
247
|
return updatedSession;
|
|
235
248
|
}
|
|
@@ -257,6 +270,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
257
270
|
resource,
|
|
258
271
|
authorizationServer: discovery.authorizationServer,
|
|
259
272
|
client: resolvedClient.client,
|
|
273
|
+
...(requestedScope === undefined ? {} : { requestedScope }),
|
|
260
274
|
discovery: toStoredDiscovery(discovery)
|
|
261
275
|
};
|
|
262
276
|
await saveSession(resource, sessionWithoutTokens);
|
|
@@ -268,7 +282,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
268
282
|
clientId: resolvedClient.client.clientId,
|
|
269
283
|
redirectUri: loopback.redirectUri,
|
|
270
284
|
codeChallenge: challenge,
|
|
271
|
-
clientMetadata
|
|
285
|
+
clientMetadata
|
|
272
286
|
});
|
|
273
287
|
const code = await loopback.waitForCode(authorizationUrl);
|
|
274
288
|
const tokens = await exchangeAuthorizationCode({
|
|
@@ -282,6 +296,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
282
296
|
fetch, signal,
|
|
283
297
|
now
|
|
284
298
|
});
|
|
299
|
+
if (requestedScope !== undefined && tokens.scope !== undefined && normalizeOAuthScope(tokens.scope) !== requestedScope)
|
|
300
|
+
throw new Error("OAuth authorization response does not match the requested OAuth scope");
|
|
285
301
|
const session = {
|
|
286
302
|
...sessionWithoutTokens,
|
|
287
303
|
tokens
|
|
@@ -313,8 +329,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
313
329
|
}
|
|
314
330
|
async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
|
|
315
331
|
parentSignal?.throwIfAborted();
|
|
316
|
-
|
|
317
|
-
if (options.client.mode === "static") {
|
|
332
|
+
if (options.client.mode === "static" || configuredClient?.registration !== undefined) {
|
|
318
333
|
if (configuredClient === null) {
|
|
319
334
|
throw new Error("OAuth client_id must not be blank");
|
|
320
335
|
}
|
|
@@ -363,7 +378,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
363
378
|
};
|
|
364
379
|
}
|
|
365
380
|
}
|
|
366
|
-
const registrationBody = buildClientRegistrationBody(
|
|
381
|
+
const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
|
|
367
382
|
const deadline = AbortSignal.timeout(30_000);
|
|
368
383
|
const signal = parentSignal === undefined ? deadline : AbortSignal.any([parentSignal, deadline]);
|
|
369
384
|
const response = await fetchMcpResponse(fetch, registrationEndpoint, {
|
|
@@ -375,16 +390,12 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
375
390
|
signal
|
|
376
391
|
});
|
|
377
392
|
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
throw new Error("OAuth client registration response missing client_id");
|
|
381
|
-
}
|
|
382
|
-
const clientSecret = getOwnString(payload, "client_secret");
|
|
393
|
+
const registration = parseOAuthClientRegistration(payload);
|
|
394
|
+
const registeredSecret = getOwnString(registration, "client_secret");
|
|
383
395
|
const registeredClient = {
|
|
384
|
-
clientId:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
: undefined
|
|
396
|
+
clientId: registration.client_id.trim(),
|
|
397
|
+
...(registeredSecret === undefined ? {} : { clientSecret: registeredSecret.trim() }),
|
|
398
|
+
registration
|
|
388
399
|
};
|
|
389
400
|
await saveRegisteredClient(discovery.authorizationServer, registeredClient);
|
|
390
401
|
return {
|
|
@@ -410,7 +421,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
410
421
|
return null;
|
|
411
422
|
}
|
|
412
423
|
const client = await clientStore.load(issuer);
|
|
413
|
-
const normalizedClient = client === null ? null :
|
|
424
|
+
const normalizedClient = client === null ? null : normalizeStoredOAuthClient(client);
|
|
414
425
|
if (client !== null && normalizedClient === null) {
|
|
415
426
|
await clientStore.clear(issuer);
|
|
416
427
|
return null;
|
|
@@ -489,7 +500,7 @@ function normalizeLoadedSession(session) {
|
|
|
489
500
|
const refreshState = getOwnEntry(session, "refreshState");
|
|
490
501
|
if (refreshState !== undefined && (refreshState !== "pending" || getOwnEntry(session, "tokens") !== undefined))
|
|
491
502
|
throw new Error("Stored OAuth refresh state is invalid");
|
|
492
|
-
const client =
|
|
503
|
+
const client = normalizeStoredOAuthClient(getOwnEntry(session, "client"));
|
|
493
504
|
if (client === null) {
|
|
494
505
|
return { ...session, client: { clientId: "" }, tokens: undefined };
|
|
495
506
|
}
|
|
@@ -499,25 +510,6 @@ function normalizeLoadedSession(session) {
|
|
|
499
510
|
tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
|
|
500
511
|
};
|
|
501
512
|
}
|
|
502
|
-
function normalizeStoredClient(value) {
|
|
503
|
-
if (!isObjectRecord(value)) {
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
const clientId = getOwnString(value, "clientId");
|
|
507
|
-
if (clientId === undefined || clientId.trim().length === 0) {
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
510
|
-
const normalizedClientId = clientId.trim();
|
|
511
|
-
const clientSecret = getOwnEntry(value, "clientSecret");
|
|
512
|
-
if (clientSecret === undefined) {
|
|
513
|
-
return { clientId: normalizedClientId };
|
|
514
|
-
}
|
|
515
|
-
if (typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
const normalizedClientSecret = clientSecret.trim();
|
|
519
|
-
return { clientId: normalizedClientId, clientSecret: normalizedClientSecret };
|
|
520
|
-
}
|
|
521
513
|
function normalizeStoredTokens(value) {
|
|
522
514
|
if (value === undefined || !isObjectRecord(value)) {
|
|
523
515
|
return undefined;
|
|
@@ -526,10 +518,10 @@ function normalizeStoredTokens(value) {
|
|
|
526
518
|
const tokenType = getOwnString(value, "tokenType");
|
|
527
519
|
const expiresAt = getOwnEntry(value, "expiresAt");
|
|
528
520
|
const refreshToken = getOwnEntry(value, "refreshToken");
|
|
529
|
-
const scope =
|
|
521
|
+
const scope = getOwnEntry(value, "scope");
|
|
530
522
|
const normalizedAccessToken = accessToken?.trim();
|
|
531
523
|
const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : undefined;
|
|
532
|
-
const normalizedScope = scope
|
|
524
|
+
const normalizedScope = normalizeOAuthScope(scope);
|
|
533
525
|
if (accessToken === undefined ||
|
|
534
526
|
normalizedAccessToken === undefined ||
|
|
535
527
|
normalizedAccessToken.length === 0 ||
|
|
@@ -561,18 +553,18 @@ function getClientMetadata(client) {
|
|
|
561
553
|
}
|
|
562
554
|
return {
|
|
563
555
|
clientName: normalizeOptionalOAuthString(client.metadata.clientName),
|
|
564
|
-
scope:
|
|
556
|
+
scope: normalizeOAuthScope(client.metadata.scope),
|
|
565
557
|
softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
|
|
566
558
|
softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
|
|
567
559
|
};
|
|
568
560
|
}
|
|
569
561
|
function normalizeConfiguredClient(client) {
|
|
570
|
-
const
|
|
571
|
-
|
|
562
|
+
const registration = client.registration === undefined ? undefined : parseOAuthClientRegistration(client.registration);
|
|
563
|
+
const clientId = normalizeOptionalOAuthString(client.clientId) ?? registration?.client_id.trim();
|
|
564
|
+
if (clientId === undefined)
|
|
572
565
|
return null;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return clientSecret === undefined ? { clientId } : { clientId, clientSecret };
|
|
566
|
+
const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === undefined ? undefined : getOwnString(registration, "client_secret")?.trim());
|
|
567
|
+
return normalizeStoredOAuthClient({ clientId, clientSecret, registration });
|
|
576
568
|
}
|
|
577
569
|
function normalizeOptionalOAuthString(value) {
|
|
578
570
|
if (value === undefined) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Compare OAuth scope sets without accepting controls or changing token case. */
|
|
2
|
+
export function normalizeOAuthScope(scope) {
|
|
3
|
+
if (scope === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
if (typeof scope !== "string" || [...scope].some(char => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
|
|
6
|
+
throw new Error("Invalid OAuth scope syntax");
|
|
7
|
+
const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
|
|
8
|
+
return normalized || undefined;
|
|
9
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
2
2
|
import { readBoundedResponseText } from "../http-response.js";
|
|
3
3
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
4
|
+
import { normalizeOAuthScope } from "./scope.js";
|
|
4
5
|
const MAX_JS_DATE_MS = 8_640_000_000_000_000;
|
|
5
6
|
export class OAuthError extends Error {
|
|
6
7
|
error;
|
|
@@ -117,7 +118,9 @@ async function requestTokens(input) {
|
|
|
117
118
|
const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0
|
|
118
119
|
? refreshToken.trim()
|
|
119
120
|
: undefined;
|
|
120
|
-
const normalizedScope =
|
|
121
|
+
const normalizedScope = normalizeOAuthScope(scope);
|
|
122
|
+
if (scope !== undefined && normalizedScope === undefined)
|
|
123
|
+
throw new Error("Invalid OAuth scope syntax in token response");
|
|
121
124
|
return {
|
|
122
125
|
accessToken: normalizedAccessToken,
|
|
123
126
|
refreshToken: normalizedRefreshToken === undefined ? undefined : normalizedRefreshToken,
|
|
@@ -70,16 +70,32 @@ export interface StoredOAuthTokens {
|
|
|
70
70
|
expiresAt: number | null;
|
|
71
71
|
scope?: string;
|
|
72
72
|
}
|
|
73
|
+
/** Full RFC 7591 response, including JSON provider extensions. */
|
|
74
|
+
export interface OAuthClientRegistration extends Record<string, unknown> {
|
|
75
|
+
client_id: string;
|
|
76
|
+
client_secret?: string | null;
|
|
77
|
+
redirect_uris?: string[] | null;
|
|
78
|
+
grant_types?: string[] | null;
|
|
79
|
+
response_types?: string[] | null;
|
|
80
|
+
contacts?: string[] | null;
|
|
81
|
+
client_id_issued_at?: number | null;
|
|
82
|
+
client_secret_expires_at?: number | null;
|
|
83
|
+
token_endpoint_auth_method?: string | null;
|
|
84
|
+
}
|
|
85
|
+
export interface StoredOAuthClient {
|
|
86
|
+
clientId: string;
|
|
87
|
+
clientSecret?: string;
|
|
88
|
+
registration?: OAuthClientRegistration;
|
|
89
|
+
}
|
|
73
90
|
export interface StoredOAuthSession {
|
|
74
91
|
resource: string;
|
|
75
92
|
authorizationServer: string;
|
|
76
|
-
client:
|
|
77
|
-
clientId: string;
|
|
78
|
-
clientSecret?: string;
|
|
79
|
-
};
|
|
93
|
+
client: StoredOAuthClient;
|
|
80
94
|
tokens?: StoredOAuthTokens;
|
|
81
95
|
/** A refresh was begun; its winning response may not have been persisted. */
|
|
82
96
|
refreshState?: "pending";
|
|
97
|
+
/** Canonical explicitly requested scope set when the server omits token scope. */
|
|
98
|
+
requestedScope?: string;
|
|
83
99
|
discovery: {
|
|
84
100
|
resourceMetadataUrl: string;
|
|
85
101
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -102,11 +118,14 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
102
118
|
clientId?: string;
|
|
103
119
|
clientSecret?: string;
|
|
104
120
|
metadata?: OAuthClientMetadata;
|
|
121
|
+
/** Import a complete registration owned by the caller. */
|
|
122
|
+
registration?: OAuthClientRegistration;
|
|
105
123
|
} | {
|
|
106
124
|
mode: "static";
|
|
107
125
|
clientId: string;
|
|
108
126
|
clientSecret?: string;
|
|
109
127
|
metadata?: OAuthClientMetadata;
|
|
128
|
+
registration?: OAuthClientRegistration;
|
|
110
129
|
};
|
|
111
130
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
112
131
|
allowInteractive?: boolean;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
|
|
2
|
+
export { parseOAuthClientRegistration } from "./client/client-registration.js";
|
|
2
3
|
export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
|
|
3
4
|
export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
|
|
4
5
|
export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
|
|
5
6
|
export { OAuthError, } from "./client/token-endpoint.js";
|
|
6
7
|
export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
|
|
7
8
|
export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
|
|
8
|
-
export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthTokens, } from "./client/types.js";
|
|
9
|
+
export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, } from "./client/types.js";
|
|
9
10
|
export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
|
|
10
11
|
export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
|
|
11
12
|
export { readBoundedResponseText } from "./http-response.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createAuthStoreSessionStore, } from "./client/auth-store-session-store.js";
|
|
2
|
+
export { parseOAuthClientRegistration } from "./client/client-registration.js";
|
|
2
3
|
export { createDefaultOAuthClientProvider, createOAuthClientProvider, } from "./client/default-oauth-client-provider.js";
|
|
3
4
|
export { buildSuccessPage, createLoopbackAuthorizationSession, extractCodeFromInput, } from "./client/loopback-authorization.js";
|
|
4
5
|
export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
|
|
@@ -173,16 +173,32 @@ interface StoredOAuthTokens {
|
|
|
173
173
|
expiresAt: number | null;
|
|
174
174
|
scope?: string;
|
|
175
175
|
}
|
|
176
|
+
/** Full RFC 7591 response, including JSON provider extensions. */
|
|
177
|
+
interface OAuthClientRegistration extends Record<string, unknown> {
|
|
178
|
+
client_id: string;
|
|
179
|
+
client_secret?: string | null;
|
|
180
|
+
redirect_uris?: string[] | null;
|
|
181
|
+
grant_types?: string[] | null;
|
|
182
|
+
response_types?: string[] | null;
|
|
183
|
+
contacts?: string[] | null;
|
|
184
|
+
client_id_issued_at?: number | null;
|
|
185
|
+
client_secret_expires_at?: number | null;
|
|
186
|
+
token_endpoint_auth_method?: string | null;
|
|
187
|
+
}
|
|
188
|
+
interface StoredOAuthClient {
|
|
189
|
+
clientId: string;
|
|
190
|
+
clientSecret?: string;
|
|
191
|
+
registration?: OAuthClientRegistration;
|
|
192
|
+
}
|
|
176
193
|
interface StoredOAuthSession {
|
|
177
194
|
resource: string;
|
|
178
195
|
authorizationServer: string;
|
|
179
|
-
client:
|
|
180
|
-
clientId: string;
|
|
181
|
-
clientSecret?: string;
|
|
182
|
-
};
|
|
196
|
+
client: StoredOAuthClient;
|
|
183
197
|
tokens?: StoredOAuthTokens;
|
|
184
198
|
/** A refresh was begun; its winning response may not have been persisted. */
|
|
185
199
|
refreshState?: "pending";
|
|
200
|
+
/** Canonical explicitly requested scope set when the server omits token scope. */
|
|
201
|
+
requestedScope?: string;
|
|
186
202
|
discovery: {
|
|
187
203
|
resourceMetadataUrl: string;
|
|
188
204
|
resourceMetadata: Record<string, unknown>;
|
|
@@ -205,11 +221,14 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
205
221
|
clientId?: string;
|
|
206
222
|
clientSecret?: string;
|
|
207
223
|
metadata?: OAuthClientMetadata;
|
|
224
|
+
/** Import a complete registration owned by the caller. */
|
|
225
|
+
registration?: OAuthClientRegistration;
|
|
208
226
|
} | {
|
|
209
227
|
mode: "static";
|
|
210
228
|
clientId: string;
|
|
211
229
|
clientSecret?: string;
|
|
212
230
|
metadata?: OAuthClientMetadata;
|
|
231
|
+
registration?: OAuthClientRegistration;
|
|
213
232
|
};
|
|
214
233
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
215
234
|
allowInteractive?: boolean;
|
|
@@ -3395,6 +3395,124 @@ var SubscriptionManager = class {
|
|
|
3395
3395
|
}
|
|
3396
3396
|
};
|
|
3397
3397
|
|
|
3398
|
+
// ../mcp-oauth/dist/client/scope.js
|
|
3399
|
+
function normalizeOAuthScope(scope) {
|
|
3400
|
+
if (scope === void 0)
|
|
3401
|
+
return void 0;
|
|
3402
|
+
if (typeof scope !== "string" || [...scope].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) > 126 || char === '"' || char === "\\"))
|
|
3403
|
+
throw new Error("Invalid OAuth scope syntax");
|
|
3404
|
+
const normalized = [...new Set(scope.split(" ").filter(Boolean))].sort().join(" ");
|
|
3405
|
+
return normalized || void 0;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// ../mcp-oauth/dist/client/client-registration.js
|
|
3409
|
+
function parseOAuthClientRegistration(value) {
|
|
3410
|
+
const invalid = () => new Error("Invalid OAuth client registration metadata");
|
|
3411
|
+
let nodes = 0;
|
|
3412
|
+
function copy(input, depth) {
|
|
3413
|
+
if (++nodes > 2e4 || depth > 64)
|
|
3414
|
+
throw invalid();
|
|
3415
|
+
if (input === null || typeof input === "boolean" || typeof input === "string")
|
|
3416
|
+
return input;
|
|
3417
|
+
if (typeof input === "number" && Number.isFinite(input))
|
|
3418
|
+
return input;
|
|
3419
|
+
if (typeof input !== "object" || input === null)
|
|
3420
|
+
throw invalid();
|
|
3421
|
+
const descriptors = Object.getOwnPropertyDescriptors(input);
|
|
3422
|
+
if (Array.isArray(input)) {
|
|
3423
|
+
const length = descriptors.length?.value;
|
|
3424
|
+
if (length > 2e4)
|
|
3425
|
+
throw invalid();
|
|
3426
|
+
const result2 = [];
|
|
3427
|
+
for (let index = 0; index < length; index++) {
|
|
3428
|
+
const descriptor = descriptors[String(index)];
|
|
3429
|
+
if (descriptor === void 0 || !Object.hasOwn(descriptor, "value"))
|
|
3430
|
+
throw invalid();
|
|
3431
|
+
result2.push(copy(descriptor.value, depth + 1));
|
|
3432
|
+
}
|
|
3433
|
+
return result2;
|
|
3434
|
+
}
|
|
3435
|
+
if (Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
|
|
3436
|
+
throw invalid();
|
|
3437
|
+
return Object.fromEntries(Object.entries(descriptors).filter(([, descriptor]) => descriptor.enumerable).map(([key2, descriptor]) => {
|
|
3438
|
+
if (!Object.hasOwn(descriptor, "value"))
|
|
3439
|
+
throw invalid();
|
|
3440
|
+
return [key2, copy(descriptor.value, depth + 1)];
|
|
3441
|
+
}));
|
|
3442
|
+
}
|
|
3443
|
+
let result;
|
|
3444
|
+
try {
|
|
3445
|
+
result = copy(value, 0);
|
|
3446
|
+
} catch {
|
|
3447
|
+
throw invalid();
|
|
3448
|
+
}
|
|
3449
|
+
if (typeof result !== "object" || result === null || Array.isArray(result))
|
|
3450
|
+
throw invalid();
|
|
3451
|
+
const record2 = result;
|
|
3452
|
+
if (!Object.hasOwn(record2, "client_id") || typeof record2.client_id !== "string" || record2.client_id.trim() === "")
|
|
3453
|
+
throw new Error("OAuth client registration response missing client_id");
|
|
3454
|
+
for (const key2 of [
|
|
3455
|
+
"client_id",
|
|
3456
|
+
"client_secret",
|
|
3457
|
+
"token_endpoint_auth_method",
|
|
3458
|
+
"application_type",
|
|
3459
|
+
"client_name",
|
|
3460
|
+
"client_uri",
|
|
3461
|
+
"logo_uri",
|
|
3462
|
+
"scope",
|
|
3463
|
+
"tos_uri",
|
|
3464
|
+
"policy_uri",
|
|
3465
|
+
"jwks_uri",
|
|
3466
|
+
"software_id",
|
|
3467
|
+
"software_version",
|
|
3468
|
+
"software_statement",
|
|
3469
|
+
"registration_access_token",
|
|
3470
|
+
"registration_client_uri",
|
|
3471
|
+
"issuer"
|
|
3472
|
+
]) {
|
|
3473
|
+
if (Object.hasOwn(record2, key2) && record2[key2] !== null && typeof record2[key2] !== "string")
|
|
3474
|
+
throw invalid();
|
|
3475
|
+
}
|
|
3476
|
+
if (typeof record2.client_secret === "string" && record2.client_secret.trim() === "")
|
|
3477
|
+
throw invalid();
|
|
3478
|
+
for (const key2 of ["redirect_uris", "grant_types", "response_types", "contacts"]) {
|
|
3479
|
+
const entry = record2[key2];
|
|
3480
|
+
if (Object.hasOwn(record2, key2) && entry !== null && (!Array.isArray(entry) || entry.some((item) => typeof item !== "string")))
|
|
3481
|
+
throw invalid();
|
|
3482
|
+
}
|
|
3483
|
+
for (const key2 of ["client_id_issued_at", "client_secret_expires_at"]) {
|
|
3484
|
+
const entry = record2[key2];
|
|
3485
|
+
if (Object.hasOwn(record2, key2) && entry !== null && (typeof entry !== "number" || !Number.isSafeInteger(entry) || entry < 0))
|
|
3486
|
+
throw invalid();
|
|
3487
|
+
}
|
|
3488
|
+
try {
|
|
3489
|
+
normalizeOAuthScope(Object.hasOwn(record2, "scope") && record2.scope !== null ? record2.scope : void 0);
|
|
3490
|
+
} catch {
|
|
3491
|
+
throw invalid();
|
|
3492
|
+
}
|
|
3493
|
+
if (Buffer.byteLength(JSON.stringify(record2), "utf8") > 64 * 1024)
|
|
3494
|
+
throw invalid();
|
|
3495
|
+
return record2;
|
|
3496
|
+
}
|
|
3497
|
+
function normalizeStoredOAuthClient(value) {
|
|
3498
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
3499
|
+
return null;
|
|
3500
|
+
const record2 = value;
|
|
3501
|
+
const clientId = Object.hasOwn(record2, "clientId") ? record2.clientId : void 0;
|
|
3502
|
+
const clientSecret = Object.hasOwn(record2, "clientSecret") ? record2.clientSecret : void 0;
|
|
3503
|
+
if (typeof clientId !== "string" || clientId.trim() === "" || clientSecret !== void 0 && (typeof clientSecret !== "string" || clientSecret.trim() === ""))
|
|
3504
|
+
return null;
|
|
3505
|
+
const client = { clientId: clientId.trim(), ...clientSecret === void 0 ? {} : { clientSecret: clientSecret.trim() } };
|
|
3506
|
+
if (Object.hasOwn(record2, "registration") && record2.registration !== void 0) {
|
|
3507
|
+
const registration = parseOAuthClientRegistration(record2.registration);
|
|
3508
|
+
const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : void 0;
|
|
3509
|
+
if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
|
|
3510
|
+
throw new Error("OAuth client registration does not match the client identity");
|
|
3511
|
+
client.registration = registration;
|
|
3512
|
+
}
|
|
3513
|
+
return client;
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3398
3516
|
// ../mcp-oauth/dist/client/auth-store-session-store.js
|
|
3399
3517
|
import crypto from "node:crypto";
|
|
3400
3518
|
import path4 from "node:path";
|
|
@@ -4128,14 +4246,8 @@ function createAuthStoreClientStore(options, namespace) {
|
|
|
4128
4246
|
} catch {
|
|
4129
4247
|
throw new Error("Stored OAuth client must be valid JSON; reset the store explicitly to recover");
|
|
4130
4248
|
}
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
const client = { clientId };
|
|
4134
|
-
if (isObjectRecord(parsed) && Object.prototype.hasOwnProperty.call(parsed, "clientSecret")) {
|
|
4135
|
-
client.clientSecret = getOwnEntry3(parsed, "clientSecret");
|
|
4136
|
-
}
|
|
4137
|
-
return client;
|
|
4138
|
-
}
|
|
4249
|
+
if (isObjectRecord(parsed) && typeof getOwnEntry3(parsed, "clientId") === "string")
|
|
4250
|
+
return parsed;
|
|
4139
4251
|
throw new Error("Stored OAuth client must be a JSON object with clientId");
|
|
4140
4252
|
},
|
|
4141
4253
|
async save(issuer, client) {
|
|
@@ -4201,14 +4313,7 @@ function isStoredOAuthSession(value) {
|
|
|
4201
4313
|
if (!isObjectRecord(value)) {
|
|
4202
4314
|
return false;
|
|
4203
4315
|
}
|
|
4204
|
-
return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") &&
|
|
4205
|
-
}
|
|
4206
|
-
function isStoredOAuthClient(value) {
|
|
4207
|
-
if (!isObjectRecord(value) || !isNonBlankOwnString(value, "clientId")) {
|
|
4208
|
-
return false;
|
|
4209
|
-
}
|
|
4210
|
-
const clientSecret = getOwnEntry3(value, "clientSecret");
|
|
4211
|
-
return clientSecret === void 0 || typeof clientSecret === "string" && clientSecret.trim().length > 0;
|
|
4316
|
+
return isNonBlankOwnString(value, "resource") && isNonBlankOwnString(value, "authorizationServer") && normalizeStoredOAuthClient(getOwnEntry3(value, "client")) !== null && isStoredOAuthDiscovery(getOwnEntry3(value, "discovery")) && (getOwnEntry3(value, "requestedScope") === void 0 || isNonBlankOwnString(value, "requestedScope")) && (getOwnEntry3(value, "refreshState") === void 0 || getOwnEntry3(value, "refreshState") === "pending" && getOwnEntry3(value, "tokens") === void 0) && isStoredOAuthTokensOrMissing(getOwnEntry3(value, "tokens"));
|
|
4212
4317
|
}
|
|
4213
4318
|
function isStoredOAuthDiscovery(value) {
|
|
4214
4319
|
if (!isObjectRecord(value)) {
|
|
@@ -4726,7 +4831,9 @@ async function requestTokens(input) {
|
|
|
4726
4831
|
const refreshToken = getOwnEntry5(payload, "refresh_token");
|
|
4727
4832
|
const scope = getOwnEntry5(payload, "scope");
|
|
4728
4833
|
const normalizedRefreshToken = typeof refreshToken === "string" && refreshToken.trim().length > 0 ? refreshToken.trim() : void 0;
|
|
4729
|
-
const normalizedScope =
|
|
4834
|
+
const normalizedScope = normalizeOAuthScope(scope);
|
|
4835
|
+
if (scope !== void 0 && normalizedScope === void 0)
|
|
4836
|
+
throw new Error("Invalid OAuth scope syntax in token response");
|
|
4730
4837
|
return {
|
|
4731
4838
|
accessToken: normalizedAccessToken,
|
|
4732
4839
|
refreshToken: normalizedRefreshToken === void 0 ? void 0 : normalizedRefreshToken,
|
|
@@ -4847,6 +4954,9 @@ function createOAuthClientProvider(options) {
|
|
|
4847
4954
|
function createDefaultOAuthClientProvider(options) {
|
|
4848
4955
|
loopbackTarget(options.browser);
|
|
4849
4956
|
assertPersistenceNamespace(options.persistenceNamespace);
|
|
4957
|
+
const clientMetadata = getClientMetadata(options.client);
|
|
4958
|
+
const requestedScope = clientMetadata?.scope;
|
|
4959
|
+
const configuredClient = normalizeConfiguredClient(options.client);
|
|
4850
4960
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
|
|
4851
4961
|
const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
|
|
4852
4962
|
const now = options.now ?? Date.now;
|
|
@@ -4864,11 +4974,13 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4864
4974
|
const initialGrant = options.initialGrant === void 0 ? void 0 : {
|
|
4865
4975
|
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
4866
4976
|
tokens: normalizeStoredTokens(options.initialGrant.tokens),
|
|
4867
|
-
client:
|
|
4977
|
+
client: configuredClient
|
|
4868
4978
|
};
|
|
4869
4979
|
if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
|
|
4870
4980
|
throw new Error("OAuth initial grant requires valid tokens and the original client ID");
|
|
4871
4981
|
if (initialGrant?.tokens !== void 0) {
|
|
4982
|
+
if (requestedScope !== void 0 && initialGrant.tokens.scope !== requestedScope)
|
|
4983
|
+
throw new Error("OAuth initial grant does not match the requested OAuth scope");
|
|
4872
4984
|
try {
|
|
4873
4985
|
new Headers({ Authorization: `Bearer ${initialGrant.tokens.accessToken}` });
|
|
4874
4986
|
} catch {
|
|
@@ -4956,6 +5068,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4956
5068
|
authorizationServer: discovery.authorizationServer,
|
|
4957
5069
|
client: initialGrant.client,
|
|
4958
5070
|
tokens: initialGrant.tokens,
|
|
5071
|
+
...requestedScope === void 0 ? {} : { requestedScope },
|
|
4959
5072
|
discovery: toStoredDiscovery(discovery)
|
|
4960
5073
|
};
|
|
4961
5074
|
await saveSession(canonicalResource, session);
|
|
@@ -4965,11 +5078,13 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4965
5078
|
if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
4966
5079
|
forceRefresh = false;
|
|
4967
5080
|
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4968
|
-
if ((options.client.mode === "static" || initialGrant !== void 0) && session !== null && (session.tokens !== void 0 || session.refreshState === "pending")) {
|
|
4969
|
-
const configured =
|
|
5081
|
+
if ((options.client.mode === "static" || configuredClient?.registration !== void 0 || initialGrant !== void 0) && session !== null && (session.tokens !== void 0 || session.refreshState === "pending")) {
|
|
5082
|
+
const configured = configuredClient;
|
|
4970
5083
|
if (configured === null || configured.clientId !== session.client.clientId || configured.clientSecret !== session.client.clientSecret)
|
|
4971
5084
|
throw new Error("Stored session belongs to a different OAuth client; use separate persistence or explicitly reset it");
|
|
4972
5085
|
}
|
|
5086
|
+
if (requestedScope !== void 0 && session?.tokens !== void 0 && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
|
|
5087
|
+
throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
|
|
4973
5088
|
if (session?.refreshState === "pending") {
|
|
4974
5089
|
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
|
|
4975
5090
|
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
@@ -5046,10 +5161,13 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5046
5161
|
...session,
|
|
5047
5162
|
tokens: {
|
|
5048
5163
|
...refreshedTokens,
|
|
5049
|
-
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
5164
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken,
|
|
5165
|
+
scope: refreshedTokens.scope ?? session.tokens.scope
|
|
5050
5166
|
},
|
|
5051
5167
|
discovery: toStoredDiscovery(discovery)
|
|
5052
5168
|
};
|
|
5169
|
+
if (requestedScope !== void 0 && normalizeOAuthScope(updatedSession.tokens?.scope ?? session.requestedScope) !== requestedScope)
|
|
5170
|
+
throw new Error("OAuth refresh response does not match the requested OAuth scope; authorize again");
|
|
5053
5171
|
await saveSession(resource, updatedSession);
|
|
5054
5172
|
return updatedSession;
|
|
5055
5173
|
}
|
|
@@ -5077,6 +5195,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5077
5195
|
resource,
|
|
5078
5196
|
authorizationServer: discovery.authorizationServer,
|
|
5079
5197
|
client: resolvedClient.client,
|
|
5198
|
+
...requestedScope === void 0 ? {} : { requestedScope },
|
|
5080
5199
|
discovery: toStoredDiscovery(discovery)
|
|
5081
5200
|
};
|
|
5082
5201
|
await saveSession(resource, sessionWithoutTokens);
|
|
@@ -5088,7 +5207,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5088
5207
|
clientId: resolvedClient.client.clientId,
|
|
5089
5208
|
redirectUri: loopback.redirectUri,
|
|
5090
5209
|
codeChallenge: challenge,
|
|
5091
|
-
clientMetadata
|
|
5210
|
+
clientMetadata
|
|
5092
5211
|
});
|
|
5093
5212
|
const code = await loopback.waitForCode(authorizationUrl);
|
|
5094
5213
|
const tokens = await exchangeAuthorizationCode({
|
|
@@ -5103,6 +5222,8 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5103
5222
|
signal,
|
|
5104
5223
|
now
|
|
5105
5224
|
});
|
|
5225
|
+
if (requestedScope !== void 0 && tokens.scope !== void 0 && normalizeOAuthScope(tokens.scope) !== requestedScope)
|
|
5226
|
+
throw new Error("OAuth authorization response does not match the requested OAuth scope");
|
|
5106
5227
|
const session = {
|
|
5107
5228
|
...sessionWithoutTokens,
|
|
5108
5229
|
tokens
|
|
@@ -5132,8 +5253,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5132
5253
|
}
|
|
5133
5254
|
async function resolveClient(existingSession, discovery, redirectUri, fetch2, parentSignal) {
|
|
5134
5255
|
parentSignal?.throwIfAborted();
|
|
5135
|
-
|
|
5136
|
-
if (options.client.mode === "static") {
|
|
5256
|
+
if (options.client.mode === "static" || configuredClient?.registration !== void 0) {
|
|
5137
5257
|
if (configuredClient === null) {
|
|
5138
5258
|
throw new Error("OAuth client_id must not be blank");
|
|
5139
5259
|
}
|
|
@@ -5180,7 +5300,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5180
5300
|
};
|
|
5181
5301
|
}
|
|
5182
5302
|
}
|
|
5183
|
-
const registrationBody = buildClientRegistrationBody(
|
|
5303
|
+
const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
|
|
5184
5304
|
const deadline = AbortSignal.timeout(3e4);
|
|
5185
5305
|
const signal = parentSignal === void 0 ? deadline : AbortSignal.any([parentSignal, deadline]);
|
|
5186
5306
|
const response = await fetchMcpResponse(fetch2, registrationEndpoint, {
|
|
@@ -5192,14 +5312,12 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5192
5312
|
signal
|
|
5193
5313
|
});
|
|
5194
5314
|
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
5195
|
-
const
|
|
5196
|
-
|
|
5197
|
-
throw new Error("OAuth client registration response missing client_id");
|
|
5198
|
-
}
|
|
5199
|
-
const clientSecret = getOwnString2(payload, "client_secret");
|
|
5315
|
+
const registration = parseOAuthClientRegistration(payload);
|
|
5316
|
+
const registeredSecret = getOwnString2(registration, "client_secret");
|
|
5200
5317
|
const registeredClient = {
|
|
5201
|
-
clientId:
|
|
5202
|
-
|
|
5318
|
+
clientId: registration.client_id.trim(),
|
|
5319
|
+
...registeredSecret === void 0 ? {} : { clientSecret: registeredSecret.trim() },
|
|
5320
|
+
registration
|
|
5203
5321
|
};
|
|
5204
5322
|
await saveRegisteredClient(discovery.authorizationServer, registeredClient);
|
|
5205
5323
|
return {
|
|
@@ -5225,7 +5343,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5225
5343
|
return null;
|
|
5226
5344
|
}
|
|
5227
5345
|
const client = await clientStore.load(issuer);
|
|
5228
|
-
const normalizedClient = client === null ? null :
|
|
5346
|
+
const normalizedClient = client === null ? null : normalizeStoredOAuthClient(client);
|
|
5229
5347
|
if (client !== null && normalizedClient === null) {
|
|
5230
5348
|
await clientStore.clear(issuer);
|
|
5231
5349
|
return null;
|
|
@@ -5298,7 +5416,7 @@ function normalizeLoadedSession(session) {
|
|
|
5298
5416
|
const refreshState = getOwnEntry6(session, "refreshState");
|
|
5299
5417
|
if (refreshState !== void 0 && (refreshState !== "pending" || getOwnEntry6(session, "tokens") !== void 0))
|
|
5300
5418
|
throw new Error("Stored OAuth refresh state is invalid");
|
|
5301
|
-
const client =
|
|
5419
|
+
const client = normalizeStoredOAuthClient(getOwnEntry6(session, "client"));
|
|
5302
5420
|
if (client === null) {
|
|
5303
5421
|
return { ...session, client: { clientId: "" }, tokens: void 0 };
|
|
5304
5422
|
}
|
|
@@ -5308,25 +5426,6 @@ function normalizeLoadedSession(session) {
|
|
|
5308
5426
|
tokens: normalizeStoredTokens(getOwnEntry6(session, "tokens"))
|
|
5309
5427
|
};
|
|
5310
5428
|
}
|
|
5311
|
-
function normalizeStoredClient(value) {
|
|
5312
|
-
if (!isObjectRecord3(value)) {
|
|
5313
|
-
return null;
|
|
5314
|
-
}
|
|
5315
|
-
const clientId = getOwnString2(value, "clientId");
|
|
5316
|
-
if (clientId === void 0 || clientId.trim().length === 0) {
|
|
5317
|
-
return null;
|
|
5318
|
-
}
|
|
5319
|
-
const normalizedClientId = clientId.trim();
|
|
5320
|
-
const clientSecret = getOwnEntry6(value, "clientSecret");
|
|
5321
|
-
if (clientSecret === void 0) {
|
|
5322
|
-
return { clientId: normalizedClientId };
|
|
5323
|
-
}
|
|
5324
|
-
if (typeof clientSecret !== "string" || clientSecret.trim().length === 0) {
|
|
5325
|
-
return null;
|
|
5326
|
-
}
|
|
5327
|
-
const normalizedClientSecret = clientSecret.trim();
|
|
5328
|
-
return { clientId: normalizedClientId, clientSecret: normalizedClientSecret };
|
|
5329
|
-
}
|
|
5330
5429
|
function normalizeStoredTokens(value) {
|
|
5331
5430
|
if (value === void 0 || !isObjectRecord3(value)) {
|
|
5332
5431
|
return void 0;
|
|
@@ -5335,10 +5434,10 @@ function normalizeStoredTokens(value) {
|
|
|
5335
5434
|
const tokenType = getOwnString2(value, "tokenType");
|
|
5336
5435
|
const expiresAt = getOwnEntry6(value, "expiresAt");
|
|
5337
5436
|
const refreshToken = getOwnEntry6(value, "refreshToken");
|
|
5338
|
-
const scope =
|
|
5437
|
+
const scope = getOwnEntry6(value, "scope");
|
|
5339
5438
|
const normalizedAccessToken = accessToken?.trim();
|
|
5340
5439
|
const normalizedRefreshToken = typeof refreshToken === "string" ? refreshToken.trim() : void 0;
|
|
5341
|
-
const normalizedScope = scope
|
|
5440
|
+
const normalizedScope = normalizeOAuthScope(scope);
|
|
5342
5441
|
if (accessToken === void 0 || normalizedAccessToken === void 0 || normalizedAccessToken.length === 0 || tokenType !== "Bearer" || !(expiresAt === null || typeof expiresAt === "number" && Number.isSafeInteger(expiresAt) && expiresAt <= MAX_JS_DATE_MS3 && Number.isFinite(new Date(expiresAt).getTime())) || refreshToken !== void 0 && (typeof refreshToken !== "string" || normalizedRefreshToken === void 0 || normalizedRefreshToken.length === 0)) {
|
|
5343
5442
|
return void 0;
|
|
5344
5443
|
}
|
|
@@ -5356,18 +5455,18 @@ function getClientMetadata(client) {
|
|
|
5356
5455
|
}
|
|
5357
5456
|
return {
|
|
5358
5457
|
clientName: normalizeOptionalOAuthString(client.metadata.clientName),
|
|
5359
|
-
scope:
|
|
5458
|
+
scope: normalizeOAuthScope(client.metadata.scope),
|
|
5360
5459
|
softwareId: normalizeOptionalOAuthString(client.metadata.softwareId),
|
|
5361
5460
|
softwareVersion: normalizeOptionalOAuthString(client.metadata.softwareVersion)
|
|
5362
5461
|
};
|
|
5363
5462
|
}
|
|
5364
5463
|
function normalizeConfiguredClient(client) {
|
|
5365
|
-
const
|
|
5366
|
-
|
|
5464
|
+
const registration = client.registration === void 0 ? void 0 : parseOAuthClientRegistration(client.registration);
|
|
5465
|
+
const clientId = normalizeOptionalOAuthString(client.clientId) ?? registration?.client_id.trim();
|
|
5466
|
+
if (clientId === void 0)
|
|
5367
5467
|
return null;
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
return clientSecret === void 0 ? { clientId } : { clientId, clientSecret };
|
|
5468
|
+
const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === void 0 ? void 0 : getOwnString2(registration, "client_secret")?.trim());
|
|
5469
|
+
return normalizeStoredOAuthClient({ clientId, clientSecret, registration });
|
|
5371
5470
|
}
|
|
5372
5471
|
function normalizeOptionalOAuthString(value) {
|
|
5373
5472
|
if (value === void 0) {
|