tiny-http-mcp-server 0.1.25 → 0.1.26
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 +11 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +159 -182
- package/node_modules/mcp-oauth/dist/client/session-transaction.d.ts +6 -0
- package/node_modules/mcp-oauth/dist/client/session-transaction.js +42 -0
- package/node_modules/mcp-oauth/dist/client/types.d.ts +7 -0
- package/node_modules/tiny-mcp-client/dist/index.d.ts +7 -0
- package/node_modules/tiny-mcp-client/dist/index.js +208 -182
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -41,6 +41,7 @@ const verifier = createJwksTokenVerifier({
|
|
|
41
41
|
- `mode: "dynamic"` with optional `metadata`
|
|
42
42
|
- `mode: "static"` with `clientId`, optional `clientSecret`, optional `metadata`
|
|
43
43
|
- `allowInteractive: false` prevents interactive login while retaining cached tokens and silent refresh
|
|
44
|
+
- `sessionLockTimeoutMs` limits acquisition waits for a session transaction lock (default 30,000 ms; integer from 1 to 2147483647)
|
|
44
45
|
- `initialGrant: { resource, tokens }` optionally imports an existing Bearer grant for one HTTP resource; requires the original client ID
|
|
45
46
|
- `browser.openBrowser(url)` optional
|
|
46
47
|
- `browser.readLine()` optional
|
|
@@ -106,6 +107,16 @@ Input tokens are copied and invalid expiry values fail before authorization.
|
|
|
106
107
|
|
|
107
108
|
`createAuthStoreSessionStore(options)` accepts the standard `auth-store` config.
|
|
108
109
|
|
|
110
|
+
Providers sharing the same `sessionStore` object serialize the complete session
|
|
111
|
+
read, refresh/authorization and persistence transaction for each resource.
|
|
112
|
+
Waiting requests can cancel or time out independently; they cannot release an
|
|
113
|
+
active owner's lock. Custom stores may implement
|
|
114
|
+
`withLock(resource, operation, { signal, timeoutMs })` to serialize the same
|
|
115
|
+
transaction across store instances or processes. The hook must honor acquisition
|
|
116
|
+
cancellation and keep the lock until the operation settles. The timeout bounds
|
|
117
|
+
acquisition, while token and browser operations retain their own deadlines.
|
|
118
|
+
Cross-process locking for the native secret-store backend is under development.
|
|
119
|
+
|
|
109
120
|
## Environment Variables
|
|
110
121
|
|
|
111
122
|
This package exposes no direct environment variables. When `authStore` is used,
|
|
@@ -7,6 +7,7 @@ import { createAuthorizationState } from "./authorization-state.js";
|
|
|
7
7
|
import { generateCodeChallenge, generateCodeVerifier } from "./pkce.js";
|
|
8
8
|
import { exchangeAuthorizationCode, OAuthError, refreshAccessToken, isRetryableOAuthError, readOAuthJsonObjectResponse } from "./token-endpoint.js";
|
|
9
9
|
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
10
|
+
import { withOAuthSessionTransaction } from "./session-transaction.js";
|
|
10
11
|
const MAX_JS_DATE_MS = 8_640_000_000_000_000;
|
|
11
12
|
export function createOAuthClientProvider(options) {
|
|
12
13
|
if (isProviderOptions(options)) {
|
|
@@ -20,8 +21,6 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
20
21
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore);
|
|
21
22
|
const now = options.now ?? Date.now;
|
|
22
23
|
const registeredClients = new Map();
|
|
23
|
-
const refreshPromises = new Map();
|
|
24
|
-
const authorizationPromises = new Map();
|
|
25
24
|
if (options.initialGrant !== undefined) {
|
|
26
25
|
let resource;
|
|
27
26
|
try {
|
|
@@ -115,205 +114,183 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
115
114
|
async function ensureAuthorizedSession(resource, discovery, fetch, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
116
115
|
signal?.throwIfAborted();
|
|
117
116
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
123
|
-
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
124
|
-
}
|
|
125
|
-
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
|
|
126
|
-
|| getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
|
|
127
|
-
|| (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
|
|
128
|
-
await clearSession(canonicalResource);
|
|
129
|
-
session = null;
|
|
130
|
-
}
|
|
131
|
-
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
132
|
-
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
133
|
-
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
134
|
-
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
135
|
-
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
136
|
-
await saveSession(canonicalResource, session);
|
|
137
|
-
initialGrantConsumed = true;
|
|
117
|
+
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
118
|
+
let session = await loadSession(canonicalResource);
|
|
119
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
120
|
+
initialGrantConsumed = true;
|
|
138
121
|
signal?.throwIfAborted();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
122
|
+
if (discovery !== undefined && getOwnString(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
123
|
+
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
124
|
+
}
|
|
125
|
+
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource
|
|
126
|
+
|| getOwnString(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer
|
|
127
|
+
|| (discovery !== undefined && discovery.authorizationServer !== session.authorizationServer))) {
|
|
128
|
+
await clearSession(canonicalResource);
|
|
129
|
+
session = null;
|
|
130
|
+
}
|
|
131
|
+
if (session === null && discovery !== undefined && !initialGrantConsumed && initialGrant?.resource === canonicalResource &&
|
|
132
|
+
initialGrant.tokens !== undefined && initialGrant.client !== null) {
|
|
133
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
134
|
+
session = { resource: canonicalResource, authorizationServer: discovery.authorizationServer,
|
|
135
|
+
client: initialGrant.client, tokens: initialGrant.tokens, discovery: toStoredDiscovery(discovery) };
|
|
136
|
+
await saveSession(canonicalResource, session);
|
|
137
|
+
initialGrantConsumed = true;
|
|
138
|
+
signal?.throwIfAborted();
|
|
139
|
+
}
|
|
140
|
+
if (forceRefresh && rejectedTokens !== undefined && (rejectedTokens === null || session?.tokens === undefined || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
141
|
+
forceRefresh = false;
|
|
142
|
+
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
143
|
+
if (session?.tokens !== undefined && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
151
144
|
return session;
|
|
152
145
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
146
|
+
if (session?.tokens?.refreshToken !== undefined &&
|
|
147
|
+
sessionDiscovery !== undefined &&
|
|
148
|
+
(forceRefresh || isExpired(session.tokens, now))) {
|
|
149
|
+
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
150
|
+
if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
|
|
151
|
+
return session;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (forceRefresh && session?.tokens !== undefined) {
|
|
155
|
+
session = clearSessionTokens(session);
|
|
156
|
+
await saveSession(canonicalResource, session);
|
|
157
|
+
}
|
|
158
|
+
if (!allowInteractive || sessionDiscovery === undefined) {
|
|
159
|
+
return session;
|
|
160
|
+
}
|
|
161
|
+
if (options.allowInteractive === false)
|
|
162
|
+
throw new Error("OAuth interactive authorization is disabled");
|
|
163
|
+
return authorizeSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
164
|
+
}, { signal, timeoutMs: options.sessionLockTimeoutMs });
|
|
164
165
|
}
|
|
165
166
|
async function refreshSession(resource, session, discovery, fetch, signal) {
|
|
166
167
|
signal?.throwIfAborted();
|
|
167
168
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return inFlight;
|
|
169
|
+
if (session.tokens?.refreshToken === undefined) {
|
|
170
|
+
return session;
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
let refreshAttempted = false;
|
|
173
|
+
let refreshedTokens;
|
|
174
|
+
while (true) {
|
|
173
175
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
refreshedTokens = await refreshAccessToken({
|
|
177
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
178
|
+
clientId: session.client.clientId,
|
|
179
|
+
clientSecret: session.client.clientSecret,
|
|
180
|
+
refreshToken: session.tokens.refreshToken,
|
|
181
|
+
resource,
|
|
182
|
+
fetch, signal,
|
|
183
|
+
now
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
signal?.throwIfAborted();
|
|
189
|
+
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
190
|
+
const clearedSession = clearSessionTokens(session);
|
|
191
|
+
await saveSession(resource, clearedSession);
|
|
192
|
+
return clearedSession;
|
|
176
193
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
refreshedTokens = await refreshAccessToken({
|
|
182
|
-
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
183
|
-
clientId: session.client.clientId,
|
|
184
|
-
clientSecret: session.client.clientSecret,
|
|
185
|
-
refreshToken: session.tokens.refreshToken,
|
|
186
|
-
resource,
|
|
187
|
-
fetch, signal,
|
|
188
|
-
now
|
|
189
|
-
});
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
signal?.throwIfAborted();
|
|
194
|
-
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
195
|
-
const clearedSession = clearSessionTokens(session);
|
|
196
|
-
await saveSession(resource, clearedSession);
|
|
197
|
-
return clearedSession;
|
|
198
|
-
}
|
|
199
|
-
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
200
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
201
|
-
await clearSession(resource);
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
205
|
-
refreshAttempted = true;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
194
|
+
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
195
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
196
|
+
await clearSession(resource);
|
|
197
|
+
return null;
|
|
210
198
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
},
|
|
217
|
-
discovery: toStoredDiscovery(discovery)
|
|
218
|
-
};
|
|
219
|
-
await saveSession(resource, updatedSession);
|
|
220
|
-
return updatedSession;
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
refreshPromises.delete(resource);
|
|
199
|
+
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
200
|
+
refreshAttempted = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
224
204
|
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
205
|
+
}
|
|
206
|
+
const updatedSession = {
|
|
207
|
+
...session,
|
|
208
|
+
tokens: {
|
|
209
|
+
...refreshedTokens,
|
|
210
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
211
|
+
},
|
|
212
|
+
discovery: toStoredDiscovery(discovery)
|
|
213
|
+
};
|
|
214
|
+
await saveSession(resource, updatedSession);
|
|
215
|
+
return updatedSession;
|
|
228
216
|
}
|
|
229
217
|
async function authorizeSession(resource, existingSession, discovery, fetch, signal) {
|
|
230
218
|
signal?.throwIfAborted();
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
219
|
+
assertS256PkceSupport(discovery.authorizationServerMetadata);
|
|
220
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
221
|
+
let currentSession = existingSession;
|
|
222
|
+
let transientRetryAttempted = false;
|
|
223
|
+
let reRegistrationAttempted = false;
|
|
224
|
+
while (true) {
|
|
225
|
+
const loopback = await createLoopbackAuthorizationSession({
|
|
226
|
+
openBrowser: options.browser.openBrowser,
|
|
227
|
+
readLine: options.browser.readLine,
|
|
228
|
+
createServer: options.browser.createServer,
|
|
229
|
+
landingPage: options.browser.landingPage,
|
|
230
|
+
redirectUri: options.browser.redirectUri,
|
|
231
|
+
signal: options.browser.signal === undefined ? signal : signal === undefined ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
|
|
232
|
+
timeoutMs: options.browser.timeoutMs
|
|
233
|
+
});
|
|
234
|
+
let resolvedClient = null;
|
|
235
|
+
try {
|
|
236
|
+
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
|
|
237
|
+
const sessionWithoutTokens = {
|
|
238
|
+
resource,
|
|
239
|
+
authorizationServer: discovery.authorizationServer,
|
|
240
|
+
client: resolvedClient.client,
|
|
241
|
+
discovery: toStoredDiscovery(discovery)
|
|
242
|
+
};
|
|
243
|
+
await saveSession(resource, sessionWithoutTokens);
|
|
244
|
+
const verifier = generateCodeVerifier();
|
|
245
|
+
const challenge = generateCodeChallenge(verifier);
|
|
246
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
247
|
+
metadata: discovery.authorizationServerMetadata,
|
|
248
|
+
resource,
|
|
249
|
+
clientId: resolvedClient.client.clientId,
|
|
250
|
+
redirectUri: loopback.redirectUri,
|
|
251
|
+
codeChallenge: challenge,
|
|
252
|
+
clientMetadata: getClientMetadata(options.client)
|
|
250
253
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
redirectUri: loopback.redirectUri,
|
|
279
|
-
resource,
|
|
280
|
-
fetch, signal,
|
|
281
|
-
now
|
|
282
|
-
});
|
|
283
|
-
const session = {
|
|
284
|
-
...sessionWithoutTokens,
|
|
285
|
-
tokens
|
|
286
|
-
};
|
|
287
|
-
await saveSession(resource, session);
|
|
288
|
-
return session;
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
signal?.throwIfAborted();
|
|
292
|
-
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
293
|
-
reRegistrationAttempted = true;
|
|
294
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
295
|
-
await clearSession(resource);
|
|
296
|
-
currentSession = null;
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
300
|
-
transientRetryAttempted = true;
|
|
301
|
-
await clearSession(resource);
|
|
302
|
-
currentSession = null;
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
throw error;
|
|
254
|
+
const code = await loopback.waitForCode(authorizationUrl);
|
|
255
|
+
const tokens = await exchangeAuthorizationCode({
|
|
256
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
257
|
+
clientId: resolvedClient.client.clientId,
|
|
258
|
+
clientSecret: resolvedClient.client.clientSecret,
|
|
259
|
+
code,
|
|
260
|
+
codeVerifier: verifier,
|
|
261
|
+
redirectUri: loopback.redirectUri,
|
|
262
|
+
resource,
|
|
263
|
+
fetch, signal,
|
|
264
|
+
now
|
|
265
|
+
});
|
|
266
|
+
const session = {
|
|
267
|
+
...sessionWithoutTokens,
|
|
268
|
+
tokens
|
|
269
|
+
};
|
|
270
|
+
await saveSession(resource, session);
|
|
271
|
+
return session;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
signal?.throwIfAborted();
|
|
275
|
+
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
276
|
+
reRegistrationAttempted = true;
|
|
277
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
278
|
+
await clearSession(resource);
|
|
279
|
+
currentSession = null;
|
|
280
|
+
continue;
|
|
306
281
|
}
|
|
307
|
-
|
|
308
|
-
|
|
282
|
+
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
283
|
+
transientRetryAttempted = true;
|
|
284
|
+
await clearSession(resource);
|
|
285
|
+
currentSession = null;
|
|
286
|
+
continue;
|
|
309
287
|
}
|
|
288
|
+
throw error;
|
|
310
289
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
authorizationPromises.set(resource, finalPromise);
|
|
316
|
-
return finalPromise;
|
|
290
|
+
finally {
|
|
291
|
+
loopback.close();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
317
294
|
}
|
|
318
295
|
async function resolveClient(existingSession, discovery, redirectUri, fetch, parentSignal) {
|
|
319
296
|
parentSignal?.throwIfAborted();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OAuthSessionStore } from "./types.js";
|
|
2
|
+
/** Serialize the complete read/redeem/write operation, with independent cancellation for waiters. */
|
|
3
|
+
export declare function withOAuthSessionTransaction<T>(store: OAuthSessionStore, resource: string, operation: () => Promise<T>, options?: {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const queues = new WeakMap();
|
|
2
|
+
/** Serialize the complete read/redeem/write operation, with independent cancellation for waiters. */
|
|
3
|
+
export async function withOAuthSessionTransaction(store, resource, operation, options = {}) {
|
|
4
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
5
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647)
|
|
6
|
+
throw new Error("sessionLockTimeoutMs must be an integer from 1 to 2147483647 milliseconds");
|
|
7
|
+
options.signal?.throwIfAborted();
|
|
8
|
+
const started = performance.now();
|
|
9
|
+
const pending = queues.get(store) ?? new Map();
|
|
10
|
+
queues.set(store, pending);
|
|
11
|
+
const previous = pending.get(resource) ?? Promise.resolve();
|
|
12
|
+
let release;
|
|
13
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
14
|
+
const tail = previous.then(() => current);
|
|
15
|
+
pending.set(resource, tail);
|
|
16
|
+
try {
|
|
17
|
+
let timer;
|
|
18
|
+
let rejectWait;
|
|
19
|
+
const waiting = new Promise((resolve, reject) => { rejectWait = reject; previous.then(resolve, reject); });
|
|
20
|
+
const abort = () => rejectWait(options.signal?.reason);
|
|
21
|
+
try {
|
|
22
|
+
timer = setTimeout(() => rejectWait(new Error("Timed out waiting for OAuth session transaction lock")), timeoutMs);
|
|
23
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
24
|
+
if (options.signal?.aborted)
|
|
25
|
+
abort();
|
|
26
|
+
await waiting;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
options.signal?.removeEventListener("abort", abort);
|
|
31
|
+
}
|
|
32
|
+
options.signal?.throwIfAborted();
|
|
33
|
+
return store.withLock === undefined ? await operation() : await store.withLock(resource, operation, {
|
|
34
|
+
signal: options.signal, timeoutMs: Math.max(0, timeoutMs - (performance.now() - started))
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
release();
|
|
39
|
+
void tail.then(() => { if (pending.get(resource) === tail)
|
|
40
|
+
pending.delete(resource); });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -88,6 +88,11 @@ export interface OAuthSessionStore {
|
|
|
88
88
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
89
89
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
90
90
|
clear(resource: string): Promise<void>;
|
|
91
|
+
/** Backend-wide lock covering a complete read/redeem/write transaction. */
|
|
92
|
+
withLock?<T>(resource: string, operation: () => Promise<T>, options: {
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
timeoutMs: number;
|
|
95
|
+
}): Promise<T>;
|
|
91
96
|
}
|
|
92
97
|
export interface DefaultOAuthClientProviderOptions {
|
|
93
98
|
client: {
|
|
@@ -103,6 +108,8 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
103
108
|
};
|
|
104
109
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
105
110
|
allowInteractive?: boolean;
|
|
111
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
112
|
+
sessionLockTimeoutMs?: number;
|
|
106
113
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
107
114
|
initialGrant?: {
|
|
108
115
|
resource: string;
|
|
@@ -165,6 +165,11 @@ interface OAuthSessionStore {
|
|
|
165
165
|
load(resource: string): Promise<StoredOAuthSession | null>;
|
|
166
166
|
save(resource: string, session: StoredOAuthSession): Promise<void>;
|
|
167
167
|
clear(resource: string): Promise<void>;
|
|
168
|
+
/** Backend-wide lock covering a complete read/redeem/write transaction. */
|
|
169
|
+
withLock?<T>(resource: string, operation: () => Promise<T>, options: {
|
|
170
|
+
signal?: AbortSignal;
|
|
171
|
+
timeoutMs: number;
|
|
172
|
+
}): Promise<T>;
|
|
168
173
|
}
|
|
169
174
|
interface DefaultOAuthClientProviderOptions {
|
|
170
175
|
client: {
|
|
@@ -180,6 +185,8 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
180
185
|
};
|
|
181
186
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
182
187
|
allowInteractive?: boolean;
|
|
188
|
+
/** Maximum wait to acquire a session transaction lock (default 30,000 ms). */
|
|
189
|
+
sessionLockTimeoutMs?: number;
|
|
183
190
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
184
191
|
initialGrant?: {
|
|
185
192
|
resource: string;
|
|
@@ -4585,6 +4585,55 @@ function normalizeBearerTokenType(value) {
|
|
|
4585
4585
|
return value.toLowerCase() === "bearer" ? "Bearer" : null;
|
|
4586
4586
|
}
|
|
4587
4587
|
|
|
4588
|
+
// ../mcp-oauth/dist/client/session-transaction.js
|
|
4589
|
+
var queues = /* @__PURE__ */ new WeakMap();
|
|
4590
|
+
async function withOAuthSessionTransaction(store, resource, operation, options = {}) {
|
|
4591
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
4592
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)
|
|
4593
|
+
throw new Error("sessionLockTimeoutMs must be an integer from 1 to 2147483647 milliseconds");
|
|
4594
|
+
options.signal?.throwIfAborted();
|
|
4595
|
+
const started = performance.now();
|
|
4596
|
+
const pending = queues.get(store) ?? /* @__PURE__ */ new Map();
|
|
4597
|
+
queues.set(store, pending);
|
|
4598
|
+
const previous = pending.get(resource) ?? Promise.resolve();
|
|
4599
|
+
let release;
|
|
4600
|
+
const current = new Promise((resolve) => {
|
|
4601
|
+
release = resolve;
|
|
4602
|
+
});
|
|
4603
|
+
const tail = previous.then(() => current);
|
|
4604
|
+
pending.set(resource, tail);
|
|
4605
|
+
try {
|
|
4606
|
+
let timer;
|
|
4607
|
+
let rejectWait;
|
|
4608
|
+
const waiting = new Promise((resolve, reject) => {
|
|
4609
|
+
rejectWait = reject;
|
|
4610
|
+
previous.then(resolve, reject);
|
|
4611
|
+
});
|
|
4612
|
+
const abort = () => rejectWait(options.signal?.reason);
|
|
4613
|
+
try {
|
|
4614
|
+
timer = setTimeout(() => rejectWait(new Error("Timed out waiting for OAuth session transaction lock")), timeoutMs);
|
|
4615
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
4616
|
+
if (options.signal?.aborted)
|
|
4617
|
+
abort();
|
|
4618
|
+
await waiting;
|
|
4619
|
+
} finally {
|
|
4620
|
+
clearTimeout(timer);
|
|
4621
|
+
options.signal?.removeEventListener("abort", abort);
|
|
4622
|
+
}
|
|
4623
|
+
options.signal?.throwIfAborted();
|
|
4624
|
+
return store.withLock === void 0 ? await operation() : await store.withLock(resource, operation, {
|
|
4625
|
+
signal: options.signal,
|
|
4626
|
+
timeoutMs: Math.max(0, timeoutMs - (performance.now() - started))
|
|
4627
|
+
});
|
|
4628
|
+
} finally {
|
|
4629
|
+
release();
|
|
4630
|
+
void tail.then(() => {
|
|
4631
|
+
if (pending.get(resource) === tail)
|
|
4632
|
+
pending.delete(resource);
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4588
4637
|
// ../mcp-oauth/dist/client/default-oauth-client-provider.js
|
|
4589
4638
|
var MAX_JS_DATE_MS3 = 864e13;
|
|
4590
4639
|
function createOAuthClientProvider(options) {
|
|
@@ -4599,8 +4648,6 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4599
4648
|
const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore);
|
|
4600
4649
|
const now = options.now ?? Date.now;
|
|
4601
4650
|
const registeredClients = /* @__PURE__ */ new Map();
|
|
4602
|
-
const refreshPromises = /* @__PURE__ */ new Map();
|
|
4603
|
-
const authorizationPromises = /* @__PURE__ */ new Map();
|
|
4604
4651
|
if (options.initialGrant !== void 0) {
|
|
4605
4652
|
let resource;
|
|
4606
4653
|
try {
|
|
@@ -4687,203 +4734,182 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4687
4734
|
async function ensureAuthorizedSession(resource, discovery, fetch2, allowInteractive, forceRefresh = false, signal, rejectedTokens) {
|
|
4688
4735
|
signal?.throwIfAborted();
|
|
4689
4736
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
4695
|
-
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
4696
|
-
}
|
|
4697
|
-
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
|
|
4698
|
-
await clearSession(canonicalResource);
|
|
4699
|
-
session = null;
|
|
4700
|
-
}
|
|
4701
|
-
if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
|
|
4702
|
-
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4703
|
-
session = {
|
|
4704
|
-
resource: canonicalResource,
|
|
4705
|
-
authorizationServer: discovery.authorizationServer,
|
|
4706
|
-
client: initialGrant.client,
|
|
4707
|
-
tokens: initialGrant.tokens,
|
|
4708
|
-
discovery: toStoredDiscovery(discovery)
|
|
4709
|
-
};
|
|
4710
|
-
await saveSession(canonicalResource, session);
|
|
4711
|
-
initialGrantConsumed = true;
|
|
4737
|
+
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
4738
|
+
let session = await loadSession(canonicalResource);
|
|
4739
|
+
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
4740
|
+
initialGrantConsumed = true;
|
|
4712
4741
|
signal?.throwIfAborted();
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4742
|
+
if (discovery !== void 0 && getOwnString2(discovery.authorizationServerMetadata, "issuer") !== discovery.authorizationServer) {
|
|
4743
|
+
throw new Error("OAuth discovery authorization-server issuer mismatch");
|
|
4744
|
+
}
|
|
4745
|
+
if (session !== null && (canonicalizeResourceIndicator(session.resource) !== canonicalResource || getOwnString2(session.discovery.authorizationServerMetadata, "issuer") !== session.authorizationServer || discovery !== void 0 && discovery.authorizationServer !== session.authorizationServer)) {
|
|
4746
|
+
await clearSession(canonicalResource);
|
|
4747
|
+
session = null;
|
|
4748
|
+
}
|
|
4749
|
+
if (session === null && discovery !== void 0 && !initialGrantConsumed && initialGrant?.resource === canonicalResource && initialGrant.tokens !== void 0 && initialGrant.client !== null) {
|
|
4750
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4751
|
+
session = {
|
|
4752
|
+
resource: canonicalResource,
|
|
4753
|
+
authorizationServer: discovery.authorizationServer,
|
|
4754
|
+
client: initialGrant.client,
|
|
4755
|
+
tokens: initialGrant.tokens,
|
|
4756
|
+
discovery: toStoredDiscovery(discovery)
|
|
4757
|
+
};
|
|
4758
|
+
await saveSession(canonicalResource, session);
|
|
4759
|
+
initialGrantConsumed = true;
|
|
4760
|
+
signal?.throwIfAborted();
|
|
4761
|
+
}
|
|
4762
|
+
if (forceRefresh && rejectedTokens !== void 0 && (rejectedTokens === null || session?.tokens === void 0 || !sameTokenGrant(session.tokens, rejectedTokens)))
|
|
4763
|
+
forceRefresh = false;
|
|
4764
|
+
const sessionDiscovery = resolveDiscovery(discovery, session);
|
|
4765
|
+
if (session?.tokens !== void 0 && !forceRefresh && !isExpired(session.tokens, now)) {
|
|
4723
4766
|
return session;
|
|
4724
4767
|
}
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4768
|
+
if (session?.tokens?.refreshToken !== void 0 && sessionDiscovery !== void 0 && (forceRefresh || isExpired(session.tokens, now))) {
|
|
4769
|
+
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
|
|
4770
|
+
if (session?.tokens !== void 0 && !isExpired(session.tokens, now)) {
|
|
4771
|
+
return session;
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
if (forceRefresh && session?.tokens !== void 0) {
|
|
4775
|
+
session = clearSessionTokens(session);
|
|
4776
|
+
await saveSession(canonicalResource, session);
|
|
4777
|
+
}
|
|
4778
|
+
if (!allowInteractive || sessionDiscovery === void 0) {
|
|
4779
|
+
return session;
|
|
4780
|
+
}
|
|
4781
|
+
if (options.allowInteractive === false)
|
|
4782
|
+
throw new Error("OAuth interactive authorization is disabled");
|
|
4783
|
+
return authorizeSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
|
|
4784
|
+
}, { signal, timeoutMs: options.sessionLockTimeoutMs });
|
|
4736
4785
|
}
|
|
4737
4786
|
async function refreshSession(resource, session, discovery, fetch2, signal) {
|
|
4738
4787
|
signal?.throwIfAborted();
|
|
4739
4788
|
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
return inFlight;
|
|
4789
|
+
if (session.tokens?.refreshToken === void 0) {
|
|
4790
|
+
return session;
|
|
4743
4791
|
}
|
|
4744
|
-
|
|
4792
|
+
let refreshAttempted = false;
|
|
4793
|
+
let refreshedTokens;
|
|
4794
|
+
while (true) {
|
|
4745
4795
|
try {
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
4773
|
-
await clearSession(resource);
|
|
4774
|
-
return null;
|
|
4775
|
-
}
|
|
4776
|
-
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
4777
|
-
refreshAttempted = true;
|
|
4778
|
-
continue;
|
|
4779
|
-
}
|
|
4780
|
-
throw error;
|
|
4781
|
-
}
|
|
4796
|
+
refreshedTokens = await refreshAccessToken({
|
|
4797
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
4798
|
+
clientId: session.client.clientId,
|
|
4799
|
+
clientSecret: session.client.clientSecret,
|
|
4800
|
+
refreshToken: session.tokens.refreshToken,
|
|
4801
|
+
resource,
|
|
4802
|
+
fetch: fetch2,
|
|
4803
|
+
signal,
|
|
4804
|
+
now
|
|
4805
|
+
});
|
|
4806
|
+
break;
|
|
4807
|
+
} catch (error) {
|
|
4808
|
+
signal?.throwIfAborted();
|
|
4809
|
+
if (error instanceof OAuthError && error.error === "invalid_grant") {
|
|
4810
|
+
const clearedSession = clearSessionTokens(session);
|
|
4811
|
+
await saveSession(resource, clearedSession);
|
|
4812
|
+
return clearedSession;
|
|
4813
|
+
}
|
|
4814
|
+
if (shouldReRegisterStoredDynamicClient(error, await loadRegisteredClient(discovery.authorizationServer), false)) {
|
|
4815
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
4816
|
+
await clearSession(resource);
|
|
4817
|
+
return null;
|
|
4818
|
+
}
|
|
4819
|
+
if (!refreshAttempted && isRetryableOAuthError(error)) {
|
|
4820
|
+
refreshAttempted = true;
|
|
4821
|
+
continue;
|
|
4782
4822
|
}
|
|
4783
|
-
|
|
4784
|
-
...session,
|
|
4785
|
-
tokens: {
|
|
4786
|
-
...refreshedTokens,
|
|
4787
|
-
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
4788
|
-
},
|
|
4789
|
-
discovery: toStoredDiscovery(discovery)
|
|
4790
|
-
};
|
|
4791
|
-
await saveSession(resource, updatedSession);
|
|
4792
|
-
return updatedSession;
|
|
4793
|
-
} finally {
|
|
4794
|
-
refreshPromises.delete(resource);
|
|
4823
|
+
throw error;
|
|
4795
4824
|
}
|
|
4796
|
-
}
|
|
4797
|
-
|
|
4798
|
-
|
|
4825
|
+
}
|
|
4826
|
+
const updatedSession = {
|
|
4827
|
+
...session,
|
|
4828
|
+
tokens: {
|
|
4829
|
+
...refreshedTokens,
|
|
4830
|
+
refreshToken: refreshedTokens.refreshToken ?? session.tokens.refreshToken
|
|
4831
|
+
},
|
|
4832
|
+
discovery: toStoredDiscovery(discovery)
|
|
4833
|
+
};
|
|
4834
|
+
await saveSession(resource, updatedSession);
|
|
4835
|
+
return updatedSession;
|
|
4799
4836
|
}
|
|
4800
4837
|
async function authorizeSession(resource, existingSession, discovery, fetch2, signal) {
|
|
4801
4838
|
signal?.throwIfAborted();
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4839
|
+
assertS256PkceSupport(discovery.authorizationServerMetadata);
|
|
4840
|
+
assertSecureOAuthFlowEndpoints(discovery.authorizationServerMetadata);
|
|
4841
|
+
let currentSession = existingSession;
|
|
4842
|
+
let transientRetryAttempted = false;
|
|
4843
|
+
let reRegistrationAttempted = false;
|
|
4844
|
+
while (true) {
|
|
4845
|
+
const loopback = await createLoopbackAuthorizationSession({
|
|
4846
|
+
openBrowser: options.browser.openBrowser,
|
|
4847
|
+
readLine: options.browser.readLine,
|
|
4848
|
+
createServer: options.browser.createServer,
|
|
4849
|
+
landingPage: options.browser.landingPage,
|
|
4850
|
+
redirectUri: options.browser.redirectUri,
|
|
4851
|
+
signal: options.browser.signal === void 0 ? signal : signal === void 0 ? options.browser.signal : AbortSignal.any([signal, options.browser.signal]),
|
|
4852
|
+
timeoutMs: options.browser.timeoutMs
|
|
4853
|
+
});
|
|
4854
|
+
let resolvedClient = null;
|
|
4855
|
+
try {
|
|
4856
|
+
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch2, signal);
|
|
4857
|
+
const sessionWithoutTokens = {
|
|
4858
|
+
resource,
|
|
4859
|
+
authorizationServer: discovery.authorizationServer,
|
|
4860
|
+
client: resolvedClient.client,
|
|
4861
|
+
discovery: toStoredDiscovery(discovery)
|
|
4862
|
+
};
|
|
4863
|
+
await saveSession(resource, sessionWithoutTokens);
|
|
4864
|
+
const verifier = generateCodeVerifier();
|
|
4865
|
+
const challenge = generateCodeChallenge(verifier);
|
|
4866
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
4867
|
+
metadata: discovery.authorizationServerMetadata,
|
|
4868
|
+
resource,
|
|
4869
|
+
clientId: resolvedClient.client.clientId,
|
|
4870
|
+
redirectUri: loopback.redirectUri,
|
|
4871
|
+
codeChallenge: challenge,
|
|
4872
|
+
clientMetadata: getClientMetadata(options.client)
|
|
4821
4873
|
});
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
const session = {
|
|
4856
|
-
...sessionWithoutTokens,
|
|
4857
|
-
tokens
|
|
4858
|
-
};
|
|
4859
|
-
await saveSession(resource, session);
|
|
4860
|
-
return session;
|
|
4861
|
-
} catch (error) {
|
|
4862
|
-
signal?.throwIfAborted();
|
|
4863
|
-
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
4864
|
-
reRegistrationAttempted = true;
|
|
4865
|
-
await clearRegisteredClient(discovery.authorizationServer);
|
|
4866
|
-
await clearSession(resource);
|
|
4867
|
-
currentSession = null;
|
|
4868
|
-
continue;
|
|
4869
|
-
}
|
|
4870
|
-
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
4871
|
-
transientRetryAttempted = true;
|
|
4872
|
-
await clearSession(resource);
|
|
4873
|
-
currentSession = null;
|
|
4874
|
-
continue;
|
|
4875
|
-
}
|
|
4876
|
-
throw error;
|
|
4877
|
-
} finally {
|
|
4878
|
-
loopback.close();
|
|
4874
|
+
const code = await loopback.waitForCode(authorizationUrl);
|
|
4875
|
+
const tokens = await exchangeAuthorizationCode({
|
|
4876
|
+
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
4877
|
+
clientId: resolvedClient.client.clientId,
|
|
4878
|
+
clientSecret: resolvedClient.client.clientSecret,
|
|
4879
|
+
code,
|
|
4880
|
+
codeVerifier: verifier,
|
|
4881
|
+
redirectUri: loopback.redirectUri,
|
|
4882
|
+
resource,
|
|
4883
|
+
fetch: fetch2,
|
|
4884
|
+
signal,
|
|
4885
|
+
now
|
|
4886
|
+
});
|
|
4887
|
+
const session = {
|
|
4888
|
+
...sessionWithoutTokens,
|
|
4889
|
+
tokens
|
|
4890
|
+
};
|
|
4891
|
+
await saveSession(resource, session);
|
|
4892
|
+
return session;
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
signal?.throwIfAborted();
|
|
4895
|
+
if (shouldReRegisterStoredDynamicClient(error, resolvedClient, reRegistrationAttempted)) {
|
|
4896
|
+
reRegistrationAttempted = true;
|
|
4897
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
4898
|
+
await clearSession(resource);
|
|
4899
|
+
currentSession = null;
|
|
4900
|
+
continue;
|
|
4901
|
+
}
|
|
4902
|
+
if (!transientRetryAttempted && isRetryableOAuthError(error)) {
|
|
4903
|
+
transientRetryAttempted = true;
|
|
4904
|
+
await clearSession(resource);
|
|
4905
|
+
currentSession = null;
|
|
4906
|
+
continue;
|
|
4879
4907
|
}
|
|
4908
|
+
throw error;
|
|
4909
|
+
} finally {
|
|
4910
|
+
loopback.close();
|
|
4880
4911
|
}
|
|
4881
|
-
}
|
|
4882
|
-
const finalPromise = promise.finally(() => {
|
|
4883
|
-
authorizationPromises.delete(resource);
|
|
4884
|
-
});
|
|
4885
|
-
authorizationPromises.set(resource, finalPromise);
|
|
4886
|
-
return finalPromise;
|
|
4912
|
+
}
|
|
4887
4913
|
}
|
|
4888
4914
|
async function resolveClient(existingSession, discovery, redirectUri, fetch2, parentSignal) {
|
|
4889
4915
|
parentSignal?.throwIfAborted();
|