@assistant-ui/react-mcp 0.1.15 → 0.1.17
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/auth/createOAuthProvider.d.ts +19 -2
- package/dist/auth/createOAuthProvider.d.ts.map +1 -1
- package/dist/auth/createOAuthProvider.js +197 -35
- package/dist/auth/createOAuthProvider.js.map +1 -1
- package/dist/auth/types.d.ts +6 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.d.ts.map +1 -1
- package/dist/hooks/useMcpOAuthCallback.js +5 -6
- package/dist/hooks/useMcpOAuthCallback.js.map +1 -1
- package/dist/resources/McpManagerResource.d.ts.map +1 -1
- package/dist/resources/McpManagerResource.js +2 -1
- package/dist/resources/McpManagerResource.js.map +1 -1
- package/dist/resources/McpServerResource.d.ts +2 -1
- package/dist/resources/McpServerResource.d.ts.map +1 -1
- package/dist/resources/McpServerResource.js +79 -19
- package/dist/resources/McpServerResource.js.map +1 -1
- package/dist/resources/storage/McpLocalStorage.d.ts +7 -0
- package/dist/resources/storage/McpLocalStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpLocalStorage.js +146 -37
- package/dist/resources/storage/McpLocalStorage.js.map +1 -1
- package/dist/resources/storage/McpMemoryStorage.d.ts.map +1 -1
- package/dist/resources/storage/McpMemoryStorage.js +9 -6
- package/dist/resources/storage/McpMemoryStorage.js.map +1 -1
- package/dist/resources/storage/types.d.ts +12 -0
- package/dist/resources/storage/types.d.ts.map +1 -1
- package/dist/utils/serverUrl.d.ts +8 -0
- package/dist/utils/serverUrl.d.ts.map +1 -0
- package/dist/utils/serverUrl.js +15 -0
- package/dist/utils/serverUrl.js.map +1 -0
- package/package.json +7 -7
- package/src/auth/createOAuthProvider.test.ts +919 -5
- package/src/auth/createOAuthProvider.ts +328 -42
- package/src/auth/types.ts +6 -1
- package/src/hooks/useMcpOAuthCallback.test.ts +74 -1
- package/src/hooks/useMcpOAuthCallback.tsx +11 -8
- package/src/resources/McpManagerResource.ts +5 -1
- package/src/resources/McpServerResource.test.ts +612 -16
- package/src/resources/McpServerResource.ts +95 -23
- package/src/resources/storage/McpLocalStorage.test.ts +97 -1
- package/src/resources/storage/McpLocalStorage.ts +90 -47
- package/src/resources/storage/McpMemoryStorage.test.ts +99 -0
- package/src/resources/storage/McpMemoryStorage.ts +23 -17
- package/src/resources/storage/types.ts +12 -0
- package/src/utils/serverUrl.test.ts +66 -0
- package/src/utils/serverUrl.ts +23 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { auth, type OAuthDiscoveryState } from "@modelcontextprotocol/client";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { MCPStorage } from "../resources/storage/types";
|
|
4
4
|
import type { MCPPersistedAuthState } from "./types";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
clearOAuthProviderAuthState,
|
|
7
|
+
createOAuthProvider,
|
|
8
|
+
} from "./createOAuthProvider";
|
|
6
9
|
|
|
7
10
|
const discoveryState: OAuthDiscoveryState = {
|
|
8
11
|
authorizationServerUrl: "https://auth.example.com",
|
|
@@ -20,6 +23,8 @@ const discoveryState: OAuthDiscoveryState = {
|
|
|
20
23
|
},
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
const serverUrl = "https://mcp.example.com/docs";
|
|
27
|
+
|
|
23
28
|
const createStorage = (initial: MCPPersistedAuthState | null = null) => {
|
|
24
29
|
let state = initial;
|
|
25
30
|
const storage: MCPStorage = {
|
|
@@ -36,18 +41,136 @@ const createStorage = (initial: MCPPersistedAuthState | null = null) => {
|
|
|
36
41
|
return { storage, getState: () => state };
|
|
37
42
|
};
|
|
38
43
|
|
|
44
|
+
const createSharedStorages = (scopeId: string) => {
|
|
45
|
+
let state: MCPPersistedAuthState | null = null;
|
|
46
|
+
const create = (): MCPStorage => ({
|
|
47
|
+
scopeId,
|
|
48
|
+
loadCustomServers: async () => [],
|
|
49
|
+
saveCustomServers: async () => {},
|
|
50
|
+
loadAuthState: async () => state,
|
|
51
|
+
saveAuthState: async (_serverId, next) => {
|
|
52
|
+
state = next;
|
|
53
|
+
},
|
|
54
|
+
clearAuthState: async () => {
|
|
55
|
+
state = null;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return { create, getState: () => state };
|
|
59
|
+
};
|
|
60
|
+
|
|
39
61
|
const createProvider = (storage: MCPStorage) =>
|
|
40
62
|
createOAuthProvider({
|
|
41
63
|
serverId: "docs",
|
|
64
|
+
serverUrl,
|
|
42
65
|
config: { type: "oauth" },
|
|
43
66
|
storage,
|
|
44
67
|
redirectUri: "http://localhost/callback",
|
|
45
68
|
onAuthorizationUrl: () => {},
|
|
46
69
|
});
|
|
47
70
|
|
|
71
|
+
const createStaticProvider = (storage: MCPStorage, clientSecret?: string) =>
|
|
72
|
+
createOAuthProvider({
|
|
73
|
+
serverId: "docs",
|
|
74
|
+
serverUrl,
|
|
75
|
+
config: {
|
|
76
|
+
type: "oauth",
|
|
77
|
+
clientId: "client-a",
|
|
78
|
+
...(clientSecret ? { clientSecret } : {}),
|
|
79
|
+
},
|
|
80
|
+
storage,
|
|
81
|
+
redirectUri: "http://localhost/callback",
|
|
82
|
+
onAuthorizationUrl: () => {},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const createStaticProviderForUrl = (storage: MCPStorage, url: string) =>
|
|
86
|
+
createOAuthProvider({
|
|
87
|
+
serverId: "docs",
|
|
88
|
+
serverUrl: url,
|
|
89
|
+
config: { type: "oauth", clientId: "client-a" },
|
|
90
|
+
storage,
|
|
91
|
+
redirectUri: "http://localhost/callback",
|
|
92
|
+
onAuthorizationUrl: () => {},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const discoveryStateFor = (issuer: string): OAuthDiscoveryState => ({
|
|
96
|
+
...discoveryState,
|
|
97
|
+
authorizationServerUrl: issuer,
|
|
98
|
+
authorizationServerMetadata: {
|
|
99
|
+
issuer,
|
|
100
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
101
|
+
token_endpoint: `${issuer}/token`,
|
|
102
|
+
registration_endpoint: `${issuer}/register`,
|
|
103
|
+
response_types_supported: ["code"],
|
|
104
|
+
code_challenge_methods_supported: ["S256"],
|
|
105
|
+
},
|
|
106
|
+
resourceMetadata: {
|
|
107
|
+
resource: "https://mcp.example.com",
|
|
108
|
+
authorization_servers: [issuer],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const rejectFetch = async () => {
|
|
113
|
+
throw new Error("Unexpected OAuth request");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
describe("createOAuthProvider callback state", () => {
|
|
117
|
+
it("persists the generated state with the PKCE verifier", async () => {
|
|
118
|
+
const { storage, getState } = createStorage();
|
|
119
|
+
const provider = createStaticProvider(storage);
|
|
120
|
+
|
|
121
|
+
const state = await provider.state?.();
|
|
122
|
+
await provider.saveCodeVerifier("pkce-verifier");
|
|
123
|
+
|
|
124
|
+
expect(state).toMatch(/^aui-mcp:ZG9jcw\./);
|
|
125
|
+
expect(getState()).toEqual({
|
|
126
|
+
serverUrl,
|
|
127
|
+
codeVerifier: "pkce-verifier",
|
|
128
|
+
state,
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("consumes callback state when tokens are saved", async () => {
|
|
133
|
+
const { storage, getState } = createStorage({
|
|
134
|
+
serverUrl,
|
|
135
|
+
codeVerifier: "pkce-verifier",
|
|
136
|
+
state: "aui-mcp:ZG9jcw.nonce",
|
|
137
|
+
});
|
|
138
|
+
const provider = createStaticProvider(storage);
|
|
139
|
+
|
|
140
|
+
await provider.saveTokens({
|
|
141
|
+
access_token: "access-token",
|
|
142
|
+
token_type: "bearer",
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(getState()).toEqual({
|
|
146
|
+
serverUrl,
|
|
147
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
148
|
+
tokensClientId: "client-a",
|
|
149
|
+
codeVerifier: "pkce-verifier",
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it.each(["verifier", "all"] as const)(
|
|
154
|
+
"clears callback state through the %s invalidation scope",
|
|
155
|
+
async (scope) => {
|
|
156
|
+
const { storage, getState } = createStorage({
|
|
157
|
+
serverUrl,
|
|
158
|
+
codeVerifier: "pkce-verifier",
|
|
159
|
+
state: "aui-mcp:ZG9jcw.nonce",
|
|
160
|
+
});
|
|
161
|
+
const provider = createProvider(storage);
|
|
162
|
+
|
|
163
|
+
await provider.invalidateCredentials?.(scope);
|
|
164
|
+
|
|
165
|
+
expect(getState()).toEqual({ serverUrl });
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
48
170
|
describe("createOAuthProvider discovery state", () => {
|
|
49
171
|
it("persists discovery state alongside the PKCE verifier", async () => {
|
|
50
172
|
const { storage, getState } = createStorage({
|
|
173
|
+
serverUrl,
|
|
51
174
|
codeVerifier: "pkce-verifier",
|
|
52
175
|
});
|
|
53
176
|
const provider = createProvider(storage);
|
|
@@ -55,13 +178,14 @@ describe("createOAuthProvider discovery state", () => {
|
|
|
55
178
|
await provider.saveDiscoveryState?.(discoveryState);
|
|
56
179
|
|
|
57
180
|
expect(getState()).toEqual({
|
|
181
|
+
serverUrl,
|
|
58
182
|
codeVerifier: "pkce-verifier",
|
|
59
183
|
discoveryState,
|
|
60
184
|
});
|
|
61
185
|
});
|
|
62
186
|
|
|
63
187
|
it("restores discovery state on the OAuth callback leg", async () => {
|
|
64
|
-
const { storage } = createStorage({ discoveryState });
|
|
188
|
+
const { storage } = createStorage({ serverUrl, discoveryState });
|
|
65
189
|
const provider = createProvider(storage);
|
|
66
190
|
|
|
67
191
|
await expect(provider.discoveryState?.()).resolves.toEqual(discoveryState);
|
|
@@ -71,6 +195,7 @@ describe("createOAuthProvider discovery state", () => {
|
|
|
71
195
|
"clears discovery state through the %s invalidation scope",
|
|
72
196
|
async (scope) => {
|
|
73
197
|
const { storage, getState } = createStorage({
|
|
198
|
+
serverUrl,
|
|
74
199
|
codeVerifier: "pkce-verifier",
|
|
75
200
|
discoveryState,
|
|
76
201
|
});
|
|
@@ -79,8 +204,797 @@ describe("createOAuthProvider discovery state", () => {
|
|
|
79
204
|
await provider.invalidateCredentials?.(scope);
|
|
80
205
|
|
|
81
206
|
expect(getState()).toEqual(
|
|
82
|
-
scope === "all"
|
|
207
|
+
scope === "all"
|
|
208
|
+
? { serverUrl }
|
|
209
|
+
: { serverUrl, codeVerifier: "pkce-verifier" },
|
|
83
210
|
);
|
|
84
211
|
},
|
|
85
212
|
);
|
|
86
213
|
});
|
|
214
|
+
|
|
215
|
+
describe("createOAuthProvider persistence", () => {
|
|
216
|
+
it("migrates unmarked OAuth credentials without losing callback state", async () => {
|
|
217
|
+
const { storage, getState } = createStorage({
|
|
218
|
+
serverUrl,
|
|
219
|
+
tokens: {
|
|
220
|
+
access_token: "legacy-access",
|
|
221
|
+
token_type: "bearer",
|
|
222
|
+
refresh_token: "legacy-refresh",
|
|
223
|
+
},
|
|
224
|
+
clientInformation: {
|
|
225
|
+
client_id: "legacy-client",
|
|
226
|
+
redirect_uris: ["http://localhost/callback"],
|
|
227
|
+
},
|
|
228
|
+
codeVerifier: "pkce-verifier",
|
|
229
|
+
state: "aui-mcp:ZG9jcw.nonce",
|
|
230
|
+
discoveryState,
|
|
231
|
+
token: "bearer-token",
|
|
232
|
+
});
|
|
233
|
+
const provider = createProvider(storage);
|
|
234
|
+
|
|
235
|
+
await expect(provider.clientInformation()).resolves.toBeUndefined();
|
|
236
|
+
await expect(provider.tokens()).resolves.toBeUndefined();
|
|
237
|
+
|
|
238
|
+
expect(getState()).toEqual({
|
|
239
|
+
serverUrl,
|
|
240
|
+
codeVerifier: "pkce-verifier",
|
|
241
|
+
state: "aui-mcp:ZG9jcw.nonce",
|
|
242
|
+
discoveryState,
|
|
243
|
+
token: "bearer-token",
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("reuses only marked dynamic credentials for the same client", async () => {
|
|
248
|
+
const clientInformation = {
|
|
249
|
+
client_id: "registered-client",
|
|
250
|
+
redirect_uris: ["http://localhost/callback"],
|
|
251
|
+
};
|
|
252
|
+
const { storage } = createStorage({
|
|
253
|
+
serverUrl,
|
|
254
|
+
clientInformation,
|
|
255
|
+
clientInformationSource: "registered",
|
|
256
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
257
|
+
tokensClientId: "registered-client",
|
|
258
|
+
});
|
|
259
|
+
const provider = createProvider(storage);
|
|
260
|
+
|
|
261
|
+
await expect(provider.clientInformation()).resolves.toEqual(
|
|
262
|
+
clientInformation,
|
|
263
|
+
);
|
|
264
|
+
await expect(provider.tokens()).resolves.toEqual({
|
|
265
|
+
access_token: "access-token",
|
|
266
|
+
token_type: "bearer",
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("drops credentials when a configured client changes", async () => {
|
|
271
|
+
const { storage, getState } = createStorage({
|
|
272
|
+
serverUrl,
|
|
273
|
+
clientInformation: {
|
|
274
|
+
client_id: "client-a",
|
|
275
|
+
redirect_uris: ["http://localhost/callback"],
|
|
276
|
+
},
|
|
277
|
+
clientInformationSource: "registered",
|
|
278
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
279
|
+
tokensClientId: "client-a",
|
|
280
|
+
});
|
|
281
|
+
const provider = createOAuthProvider({
|
|
282
|
+
serverId: "docs",
|
|
283
|
+
serverUrl,
|
|
284
|
+
config: { type: "oauth", clientId: "client-b" },
|
|
285
|
+
storage,
|
|
286
|
+
redirectUri: "http://localhost/callback",
|
|
287
|
+
onAuthorizationUrl: () => {},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
await expect(provider.clientInformation()).resolves.toEqual({
|
|
291
|
+
client_id: "client-b",
|
|
292
|
+
redirect_uris: ["http://localhost/callback"],
|
|
293
|
+
});
|
|
294
|
+
await expect(provider.tokens()).resolves.toBeUndefined();
|
|
295
|
+
expect(getState()).toEqual({ serverUrl });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("keeps a registered client when static config uses the same client", async () => {
|
|
299
|
+
const clientInformation = {
|
|
300
|
+
client_id: "client-a",
|
|
301
|
+
redirect_uris: ["http://localhost/callback"],
|
|
302
|
+
};
|
|
303
|
+
const { storage, getState } = createStorage({
|
|
304
|
+
serverUrl,
|
|
305
|
+
clientInformation,
|
|
306
|
+
clientInformationSource: "registered",
|
|
307
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
308
|
+
tokensClientId: "client-a",
|
|
309
|
+
});
|
|
310
|
+
const provider = createStaticProvider(storage);
|
|
311
|
+
|
|
312
|
+
await provider.discoveryState?.();
|
|
313
|
+
|
|
314
|
+
expect(getState()).toEqual({
|
|
315
|
+
serverUrl,
|
|
316
|
+
clientInformation,
|
|
317
|
+
clientInformationSource: "registered",
|
|
318
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
319
|
+
tokensClientId: "client-a",
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("keeps a retained registration when only the tokens are migrated away", async () => {
|
|
324
|
+
const clientInformation = {
|
|
325
|
+
client_id: "client-a",
|
|
326
|
+
redirect_uris: ["http://localhost/callback"],
|
|
327
|
+
};
|
|
328
|
+
const { storage, getState } = createStorage({
|
|
329
|
+
serverUrl,
|
|
330
|
+
clientInformation,
|
|
331
|
+
clientInformationSource: "registered",
|
|
332
|
+
tokens: { access_token: "unbound", token_type: "bearer" },
|
|
333
|
+
});
|
|
334
|
+
const provider = createStaticProvider(storage);
|
|
335
|
+
|
|
336
|
+
await provider.discoveryState?.();
|
|
337
|
+
|
|
338
|
+
expect(getState()).toEqual({
|
|
339
|
+
serverUrl,
|
|
340
|
+
clientInformation,
|
|
341
|
+
clientInformationSource: "registered",
|
|
342
|
+
});
|
|
343
|
+
expect(await provider.tokens()).toBeUndefined();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("does not let the migration write overwrite a concurrent save", async () => {
|
|
347
|
+
let state: MCPPersistedAuthState | null = {
|
|
348
|
+
serverUrl,
|
|
349
|
+
clientInformation: {
|
|
350
|
+
client_id: "legacy",
|
|
351
|
+
redirect_uris: ["http://localhost/callback"],
|
|
352
|
+
},
|
|
353
|
+
tokens: { access_token: "legacy", token_type: "bearer" },
|
|
354
|
+
};
|
|
355
|
+
let releaseFirstWrite = () => {};
|
|
356
|
+
const firstWriteGate = new Promise<void>((resolve) => {
|
|
357
|
+
releaseFirstWrite = resolve;
|
|
358
|
+
});
|
|
359
|
+
let firstWriteSeen = false;
|
|
360
|
+
const create = (): MCPStorage => ({
|
|
361
|
+
scopeId: "migration-race",
|
|
362
|
+
loadCustomServers: async () => [],
|
|
363
|
+
saveCustomServers: async () => {},
|
|
364
|
+
loadAuthState: async () => state,
|
|
365
|
+
saveAuthState: async (_serverId, next) => {
|
|
366
|
+
if (!firstWriteSeen) {
|
|
367
|
+
firstWriteSeen = true;
|
|
368
|
+
await firstWriteGate;
|
|
369
|
+
}
|
|
370
|
+
state = next;
|
|
371
|
+
},
|
|
372
|
+
clearAuthState: async () => {
|
|
373
|
+
state = null;
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const migrating = createProvider(create()).tokens();
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
379
|
+
const saving = createProvider(create()).saveCodeVerifier("verifier-xyz");
|
|
380
|
+
setTimeout(releaseFirstWrite, 20);
|
|
381
|
+
await migrating;
|
|
382
|
+
await saving;
|
|
383
|
+
|
|
384
|
+
expect(state).toEqual({ serverUrl, codeVerifier: "verifier-xyz" });
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("keeps a non-persistable re-registration out of storage", async () => {
|
|
388
|
+
const { storage, getState } = createStorage({ serverUrl });
|
|
389
|
+
const provider = createStaticProvider(storage);
|
|
390
|
+
|
|
391
|
+
await provider.saveClientInformation?.({
|
|
392
|
+
client_id: "registered-client",
|
|
393
|
+
redirect_uris: ["http://localhost/callback"],
|
|
394
|
+
});
|
|
395
|
+
await provider.saveTokens({
|
|
396
|
+
access_token: "minted-for-registered",
|
|
397
|
+
token_type: "bearer",
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(getState()).toEqual({ serverUrl });
|
|
401
|
+
await expect(provider.tokens()).resolves.toEqual({
|
|
402
|
+
access_token: "minted-for-registered",
|
|
403
|
+
token_type: "bearer",
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("still reads a sanitized cache when the migration write fails", async () => {
|
|
408
|
+
const storage: MCPStorage = {
|
|
409
|
+
loadCustomServers: async () => [],
|
|
410
|
+
saveCustomServers: async () => {},
|
|
411
|
+
loadAuthState: async () => ({
|
|
412
|
+
serverUrl,
|
|
413
|
+
clientInformation: {
|
|
414
|
+
client_id: "legacy",
|
|
415
|
+
redirect_uris: ["http://localhost/callback"],
|
|
416
|
+
},
|
|
417
|
+
tokens: { access_token: "legacy", token_type: "bearer" },
|
|
418
|
+
}),
|
|
419
|
+
saveAuthState: async () => {
|
|
420
|
+
throw new Error("storage unavailable");
|
|
421
|
+
},
|
|
422
|
+
clearAuthState: async () => {},
|
|
423
|
+
};
|
|
424
|
+
const provider = createProvider(storage);
|
|
425
|
+
|
|
426
|
+
await expect(provider.tokens()).resolves.toBeUndefined();
|
|
427
|
+
await expect(provider.clientInformation()).resolves.toBeUndefined();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("drops tokens when dynamic registration replaces the client", async () => {
|
|
431
|
+
const { storage, getState } = createStorage({
|
|
432
|
+
serverUrl,
|
|
433
|
+
clientInformation: {
|
|
434
|
+
client_id: "client-a",
|
|
435
|
+
redirect_uris: ["http://localhost/callback"],
|
|
436
|
+
},
|
|
437
|
+
clientInformationSource: "registered",
|
|
438
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
439
|
+
tokensClientId: "client-a",
|
|
440
|
+
});
|
|
441
|
+
const provider = createProvider(storage);
|
|
442
|
+
|
|
443
|
+
await provider.clientInformation();
|
|
444
|
+
await provider.saveClientInformation?.({
|
|
445
|
+
client_id: "client-b",
|
|
446
|
+
redirect_uris: ["http://localhost/callback"],
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await expect(provider.tokens()).resolves.toBeUndefined();
|
|
450
|
+
expect(getState()).toEqual({
|
|
451
|
+
serverUrl,
|
|
452
|
+
clientInformation: {
|
|
453
|
+
client_id: "client-b",
|
|
454
|
+
redirect_uris: ["http://localhost/callback"],
|
|
455
|
+
},
|
|
456
|
+
clientInformationSource: "registered",
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("binds newly saved tokens to the effective client", async () => {
|
|
461
|
+
const { storage, getState } = createStorage();
|
|
462
|
+
const provider = createProvider(storage);
|
|
463
|
+
|
|
464
|
+
await provider.saveClientInformation?.({
|
|
465
|
+
client_id: "registered-client",
|
|
466
|
+
redirect_uris: ["http://localhost/callback"],
|
|
467
|
+
});
|
|
468
|
+
await provider.saveTokens({
|
|
469
|
+
access_token: "access-token",
|
|
470
|
+
token_type: "bearer",
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(getState()).toMatchObject({
|
|
474
|
+
clientInformationSource: "registered",
|
|
475
|
+
tokensClientId: "registered-client",
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("does not reuse authentication saved for a different server URL", async () => {
|
|
480
|
+
const { storage } = createStorage({
|
|
481
|
+
serverUrl: "https://endpoint-a.example.com/mcp",
|
|
482
|
+
tokens: { access_token: "endpoint-a-token", token_type: "bearer" },
|
|
483
|
+
});
|
|
484
|
+
const provider = createOAuthProvider({
|
|
485
|
+
serverId: "docs",
|
|
486
|
+
serverUrl: "https://endpoint-b.example.com/mcp",
|
|
487
|
+
config: { type: "oauth" },
|
|
488
|
+
storage,
|
|
489
|
+
redirectUri: "http://localhost/callback",
|
|
490
|
+
onAuthorizationUrl: () => {},
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await expect(provider.tokens()).resolves.toBeUndefined();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("keeps in-memory authentication scoped to its server URL", async () => {
|
|
497
|
+
const { storage } = createStorage();
|
|
498
|
+
const endpointA = createStaticProviderForUrl(
|
|
499
|
+
storage,
|
|
500
|
+
"https://endpoint-a.example.com/mcp",
|
|
501
|
+
);
|
|
502
|
+
await endpointA.saveTokens({
|
|
503
|
+
access_token: "endpoint-a-token",
|
|
504
|
+
token_type: "bearer",
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const endpointB = createStaticProviderForUrl(
|
|
508
|
+
storage,
|
|
509
|
+
"https://endpoint-b.example.com/mcp",
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
await expect(endpointB.tokens()).resolves.toBeUndefined();
|
|
513
|
+
await endpointB.saveTokens({
|
|
514
|
+
access_token: "endpoint-b-token",
|
|
515
|
+
token_type: "bearer",
|
|
516
|
+
});
|
|
517
|
+
await expect(endpointA.tokens()).resolves.toBeUndefined();
|
|
518
|
+
await expect(endpointB.tokens()).resolves.toEqual({
|
|
519
|
+
access_token: "endpoint-b-token",
|
|
520
|
+
token_type: "bearer",
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it("waits for pending writes before reloading a previous endpoint", async () => {
|
|
525
|
+
const { storage } = createStorage();
|
|
526
|
+
const loadAuthState = vi.spyOn(storage, "loadAuthState");
|
|
527
|
+
const saveAuthState = storage.saveAuthState;
|
|
528
|
+
let releaseWrite!: () => void;
|
|
529
|
+
storage.saveAuthState = async (serverId, next) => {
|
|
530
|
+
await new Promise<void>((resolve) => {
|
|
531
|
+
releaseWrite = resolve;
|
|
532
|
+
});
|
|
533
|
+
await saveAuthState(serverId, next);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const endpointA = createStaticProviderForUrl(
|
|
537
|
+
storage,
|
|
538
|
+
"https://endpoint-a.example.com/mcp",
|
|
539
|
+
);
|
|
540
|
+
await endpointA.tokens();
|
|
541
|
+
const pendingSave = endpointA.saveTokens({
|
|
542
|
+
access_token: "endpoint-a-token",
|
|
543
|
+
token_type: "bearer",
|
|
544
|
+
});
|
|
545
|
+
await vi.waitFor(() => expect(releaseWrite).toBeDefined());
|
|
546
|
+
|
|
547
|
+
createStaticProviderForUrl(storage, "https://endpoint-b.example.com/mcp");
|
|
548
|
+
const replacementA = createStaticProviderForUrl(
|
|
549
|
+
storage,
|
|
550
|
+
"https://endpoint-a.example.com/mcp",
|
|
551
|
+
);
|
|
552
|
+
const tokens = replacementA.tokens();
|
|
553
|
+
|
|
554
|
+
await Promise.resolve();
|
|
555
|
+
expect(loadAuthState).toHaveBeenCalledTimes(1);
|
|
556
|
+
|
|
557
|
+
releaseWrite();
|
|
558
|
+
await pendingSave;
|
|
559
|
+
await expect(tokens).resolves.toEqual({
|
|
560
|
+
access_token: "endpoint-a-token",
|
|
561
|
+
token_type: "bearer",
|
|
562
|
+
});
|
|
563
|
+
expect(loadAuthState).toHaveBeenCalledTimes(2);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("does not reuse unbound legacy OAuth authentication", async () => {
|
|
567
|
+
const { storage } = createStorage({
|
|
568
|
+
tokens: { access_token: "legacy-token", token_type: "bearer" },
|
|
569
|
+
});
|
|
570
|
+
const saveAuthState = vi.spyOn(storage, "saveAuthState");
|
|
571
|
+
const provider = createStaticProvider(storage);
|
|
572
|
+
|
|
573
|
+
await expect(provider.tokens()).resolves.toBeUndefined();
|
|
574
|
+
expect(saveAuthState).not.toHaveBeenCalled();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("loads persisted auth state once for concurrent reads", async () => {
|
|
578
|
+
let resolveLoad!: (value: MCPPersistedAuthState | null) => void;
|
|
579
|
+
const loadAuthState = vi.fn(
|
|
580
|
+
() =>
|
|
581
|
+
new Promise<MCPPersistedAuthState | null>((resolve) => {
|
|
582
|
+
resolveLoad = resolve;
|
|
583
|
+
}),
|
|
584
|
+
);
|
|
585
|
+
const { storage } = createStorage();
|
|
586
|
+
storage.loadAuthState = loadAuthState;
|
|
587
|
+
const provider = createStaticProvider(storage);
|
|
588
|
+
|
|
589
|
+
const tokens = provider.tokens();
|
|
590
|
+
const clientInformation = provider.clientInformation();
|
|
591
|
+
|
|
592
|
+
await vi.waitFor(() => expect(loadAuthState).toHaveBeenCalledTimes(1));
|
|
593
|
+
resolveLoad(null);
|
|
594
|
+
await Promise.all([tokens, clientInformation]);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("retries loading persisted auth state after a failure", async () => {
|
|
598
|
+
const failure = new Error("storage unavailable");
|
|
599
|
+
const loadAuthState = vi
|
|
600
|
+
.fn<() => Promise<MCPPersistedAuthState | null>>()
|
|
601
|
+
.mockRejectedValueOnce(failure)
|
|
602
|
+
.mockResolvedValueOnce({ serverUrl, codeVerifier: "pkce-verifier" });
|
|
603
|
+
const { storage } = createStorage();
|
|
604
|
+
storage.loadAuthState = loadAuthState;
|
|
605
|
+
const provider = createProvider(storage);
|
|
606
|
+
|
|
607
|
+
const tokens = provider.tokens();
|
|
608
|
+
const clientInformation = provider.clientInformation();
|
|
609
|
+
|
|
610
|
+
await expect(tokens).rejects.toBe(failure);
|
|
611
|
+
await expect(clientInformation).rejects.toBe(failure);
|
|
612
|
+
expect(loadAuthState).toHaveBeenCalledTimes(1);
|
|
613
|
+
|
|
614
|
+
await expect(provider.codeVerifier()).resolves.toBe("pkce-verifier");
|
|
615
|
+
expect(loadAuthState).toHaveBeenCalledTimes(2);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("serializes writes so newer auth state is not overwritten", async () => {
|
|
619
|
+
const { storage } = createStorage();
|
|
620
|
+
const pendingWrites: Array<() => void> = [];
|
|
621
|
+
let persisted: MCPPersistedAuthState | null = null;
|
|
622
|
+
storage.saveAuthState = async (_serverId, next) => {
|
|
623
|
+
await new Promise<void>((resolve) => pendingWrites.push(resolve));
|
|
624
|
+
persisted = next;
|
|
625
|
+
};
|
|
626
|
+
const provider = createStaticProvider(storage);
|
|
627
|
+
await provider.tokens();
|
|
628
|
+
|
|
629
|
+
const tokenSave = provider.saveTokens({
|
|
630
|
+
access_token: "access-token",
|
|
631
|
+
token_type: "bearer",
|
|
632
|
+
});
|
|
633
|
+
await vi.waitFor(() => expect(pendingWrites).toHaveLength(1));
|
|
634
|
+
|
|
635
|
+
const verifierSave = provider.saveCodeVerifier("pkce-verifier");
|
|
636
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
637
|
+
expect(pendingWrites).toHaveLength(1);
|
|
638
|
+
|
|
639
|
+
pendingWrites.shift()!();
|
|
640
|
+
await vi.waitFor(() => expect(pendingWrites).toHaveLength(1));
|
|
641
|
+
pendingWrites.shift()!();
|
|
642
|
+
await Promise.all([tokenSave, verifierSave]);
|
|
643
|
+
|
|
644
|
+
expect(persisted).toEqual({
|
|
645
|
+
serverUrl,
|
|
646
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
647
|
+
tokensClientId: "client-a",
|
|
648
|
+
codeVerifier: "pkce-verifier",
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it("continues persisting after a failed auth state write", async () => {
|
|
653
|
+
const { storage } = createStorage();
|
|
654
|
+
const failure = new Error("storage unavailable");
|
|
655
|
+
let saveCount = 0;
|
|
656
|
+
let persisted: MCPPersistedAuthState | null = null;
|
|
657
|
+
let rejectFirstSave!: (reason: unknown) => void;
|
|
658
|
+
storage.saveAuthState = async (_serverId, next) => {
|
|
659
|
+
saveCount += 1;
|
|
660
|
+
if (saveCount === 1) {
|
|
661
|
+
await new Promise<void>((_resolve, reject) => {
|
|
662
|
+
rejectFirstSave = reject;
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
persisted = next;
|
|
666
|
+
};
|
|
667
|
+
const provider = createStaticProvider(storage);
|
|
668
|
+
await provider.tokens();
|
|
669
|
+
|
|
670
|
+
const tokenSave = provider.saveTokens({
|
|
671
|
+
access_token: "access-token",
|
|
672
|
+
token_type: "bearer",
|
|
673
|
+
});
|
|
674
|
+
const verifierSave = provider.saveCodeVerifier("pkce-verifier");
|
|
675
|
+
|
|
676
|
+
await vi.waitFor(() => expect(saveCount).toBe(1));
|
|
677
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
678
|
+
expect(saveCount).toBe(1);
|
|
679
|
+
|
|
680
|
+
const tokenSaveResult = expect(tokenSave).rejects.toBe(failure);
|
|
681
|
+
rejectFirstSave(failure);
|
|
682
|
+
await tokenSaveResult;
|
|
683
|
+
await expect(verifierSave).resolves.toBeUndefined();
|
|
684
|
+
expect(saveCount).toBe(2);
|
|
685
|
+
expect(persisted).toEqual({
|
|
686
|
+
serverUrl,
|
|
687
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
688
|
+
tokensClientId: "client-a",
|
|
689
|
+
codeVerifier: "pkce-verifier",
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
describe("createOAuthProvider persistence across provider instances", () => {
|
|
695
|
+
it("shares one auth state load across provider instances", async () => {
|
|
696
|
+
let resolveLoad!: (value: MCPPersistedAuthState | null) => void;
|
|
697
|
+
const loadAuthState = vi.fn(
|
|
698
|
+
() =>
|
|
699
|
+
new Promise<MCPPersistedAuthState | null>((resolve) => {
|
|
700
|
+
resolveLoad = resolve;
|
|
701
|
+
}),
|
|
702
|
+
);
|
|
703
|
+
const { storage } = createStorage();
|
|
704
|
+
storage.loadAuthState = loadAuthState;
|
|
705
|
+
const provider = createStaticProvider(storage);
|
|
706
|
+
const replacementProvider = createStaticProvider(storage);
|
|
707
|
+
|
|
708
|
+
const tokens = provider.tokens();
|
|
709
|
+
const clientInformation = replacementProvider.clientInformation();
|
|
710
|
+
|
|
711
|
+
await vi.waitFor(() => expect(loadAuthState).toHaveBeenCalledTimes(1));
|
|
712
|
+
resolveLoad(null);
|
|
713
|
+
await Promise.all([tokens, clientInformation]);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("serializes writes across provider instances", async () => {
|
|
717
|
+
const { storage } = createStorage();
|
|
718
|
+
const pendingWrites: Array<() => void> = [];
|
|
719
|
+
let persisted: MCPPersistedAuthState | null = null;
|
|
720
|
+
storage.saveAuthState = async (_serverId, next) => {
|
|
721
|
+
await new Promise<void>((resolve) => pendingWrites.push(resolve));
|
|
722
|
+
persisted = next;
|
|
723
|
+
};
|
|
724
|
+
const provider = createStaticProvider(storage);
|
|
725
|
+
const replacementProvider = createStaticProvider(storage);
|
|
726
|
+
await Promise.all([provider.tokens(), replacementProvider.tokens()]);
|
|
727
|
+
|
|
728
|
+
const tokenSave = provider.saveTokens({
|
|
729
|
+
access_token: "access-token",
|
|
730
|
+
token_type: "bearer",
|
|
731
|
+
});
|
|
732
|
+
await vi.waitFor(() => expect(pendingWrites).toHaveLength(1));
|
|
733
|
+
|
|
734
|
+
const verifierSave = replacementProvider.saveCodeVerifier("pkce-verifier");
|
|
735
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
736
|
+
expect(pendingWrites).toHaveLength(1);
|
|
737
|
+
|
|
738
|
+
pendingWrites.shift()!();
|
|
739
|
+
await vi.waitFor(() => expect(pendingWrites).toHaveLength(1));
|
|
740
|
+
pendingWrites.shift()!();
|
|
741
|
+
await Promise.all([tokenSave, verifierSave]);
|
|
742
|
+
|
|
743
|
+
expect(persisted).toEqual({
|
|
744
|
+
serverUrl,
|
|
745
|
+
tokens: { access_token: "access-token", token_type: "bearer" },
|
|
746
|
+
tokensClientId: "client-a",
|
|
747
|
+
codeVerifier: "pkce-verifier",
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("clears after a pending write and fences the discarded provider", async () => {
|
|
752
|
+
const { storage, getState } = createStorage();
|
|
753
|
+
const pendingWrites: Array<() => void> = [];
|
|
754
|
+
const saveAuthState = storage.saveAuthState;
|
|
755
|
+
storage.saveAuthState = async (serverId, next) => {
|
|
756
|
+
await new Promise<void>((resolve) => pendingWrites.push(resolve));
|
|
757
|
+
await saveAuthState(serverId, next);
|
|
758
|
+
};
|
|
759
|
+
const clearAuthState = vi.spyOn(storage, "clearAuthState");
|
|
760
|
+
const provider = createStaticProvider(storage);
|
|
761
|
+
await provider.tokens();
|
|
762
|
+
|
|
763
|
+
const save = provider.saveTokens({
|
|
764
|
+
access_token: "access-token",
|
|
765
|
+
token_type: "bearer",
|
|
766
|
+
});
|
|
767
|
+
await vi.waitFor(() => expect(pendingWrites).toHaveLength(1));
|
|
768
|
+
|
|
769
|
+
const clear = clearOAuthProviderAuthState(storage, "docs");
|
|
770
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
771
|
+
expect(clearAuthState).not.toHaveBeenCalled();
|
|
772
|
+
|
|
773
|
+
pendingWrites.shift()!();
|
|
774
|
+
await expect(save).resolves.toBeUndefined();
|
|
775
|
+
await clear;
|
|
776
|
+
expect(clearAuthState).toHaveBeenCalledTimes(1);
|
|
777
|
+
expect(getState()).toBeNull();
|
|
778
|
+
|
|
779
|
+
storage.saveAuthState = saveAuthState;
|
|
780
|
+
await provider.saveCodeVerifier("late-verifier");
|
|
781
|
+
expect(getState()).toBeNull();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("fences a queued write when clearing through a same-scope storage", async () => {
|
|
785
|
+
const { create, getState } = createSharedStorages("same-scope-clear");
|
|
786
|
+
const storage = create();
|
|
787
|
+
const replacement = create();
|
|
788
|
+
const pendingWrites: Array<() => void> = [];
|
|
789
|
+
const saveAuthState = storage.saveAuthState;
|
|
790
|
+
storage.saveAuthState = async (serverId, next) => {
|
|
791
|
+
await new Promise<void>((resolve) => pendingWrites.push(resolve));
|
|
792
|
+
await saveAuthState(serverId, next);
|
|
793
|
+
};
|
|
794
|
+
const clearAuthState = vi.spyOn(replacement, "clearAuthState");
|
|
795
|
+
const provider = createStaticProvider(storage);
|
|
796
|
+
await provider.tokens();
|
|
797
|
+
|
|
798
|
+
const save = provider.saveTokens({
|
|
799
|
+
access_token: "access-token",
|
|
800
|
+
token_type: "bearer",
|
|
801
|
+
});
|
|
802
|
+
await vi.waitFor(() => expect(pendingWrites).toHaveLength(1));
|
|
803
|
+
|
|
804
|
+
const clear = clearOAuthProviderAuthState(replacement, "docs");
|
|
805
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
806
|
+
expect(clearAuthState).not.toHaveBeenCalled();
|
|
807
|
+
|
|
808
|
+
pendingWrites.shift()!();
|
|
809
|
+
await expect(save).resolves.toBeUndefined();
|
|
810
|
+
await clear;
|
|
811
|
+
expect(clearAuthState).toHaveBeenCalledTimes(1);
|
|
812
|
+
expect(getState()).toBeNull();
|
|
813
|
+
|
|
814
|
+
storage.saveAuthState = saveAuthState;
|
|
815
|
+
await provider.saveCodeVerifier("late-verifier");
|
|
816
|
+
expect(getState()).toBeNull();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("keeps differently-scoped storages on separate persistence", async () => {
|
|
820
|
+
const first = createSharedStorages("scope-a");
|
|
821
|
+
const second = createSharedStorages("scope-b");
|
|
822
|
+
const firstProvider = createStaticProvider(first.create());
|
|
823
|
+
const secondProvider = createStaticProvider(second.create());
|
|
824
|
+
|
|
825
|
+
await firstProvider.saveTokens({
|
|
826
|
+
access_token: "first-token",
|
|
827
|
+
token_type: "bearer",
|
|
828
|
+
});
|
|
829
|
+
await secondProvider.saveTokens({
|
|
830
|
+
access_token: "second-token",
|
|
831
|
+
token_type: "bearer",
|
|
832
|
+
});
|
|
833
|
+
await clearOAuthProviderAuthState(second.create(), "docs");
|
|
834
|
+
await firstProvider.saveCodeVerifier("first-verifier");
|
|
835
|
+
|
|
836
|
+
expect(first.getState()).toEqual({
|
|
837
|
+
serverUrl,
|
|
838
|
+
tokens: { access_token: "first-token", token_type: "bearer" },
|
|
839
|
+
tokensClientId: "client-a",
|
|
840
|
+
codeVerifier: "first-verifier",
|
|
841
|
+
});
|
|
842
|
+
expect(second.getState()).toBeNull();
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("re-derives static client information for a replacement provider", async () => {
|
|
846
|
+
const { storage } = createStorage();
|
|
847
|
+
const provider = createOAuthProvider({
|
|
848
|
+
serverId: "docs",
|
|
849
|
+
serverUrl,
|
|
850
|
+
config: { type: "oauth", clientId: "client-a" },
|
|
851
|
+
storage,
|
|
852
|
+
redirectUri: "http://localhost/callback",
|
|
853
|
+
onAuthorizationUrl: () => {},
|
|
854
|
+
});
|
|
855
|
+
await expect(provider.clientInformation()).resolves.toEqual({
|
|
856
|
+
client_id: "client-a",
|
|
857
|
+
redirect_uris: ["http://localhost/callback"],
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const replacementProvider = createOAuthProvider({
|
|
861
|
+
serverId: "docs",
|
|
862
|
+
serverUrl,
|
|
863
|
+
config: { type: "oauth", clientId: "client-b", clientSecret: "secret-b" },
|
|
864
|
+
storage,
|
|
865
|
+
redirectUri: "http://localhost/callback-2",
|
|
866
|
+
onAuthorizationUrl: () => {},
|
|
867
|
+
});
|
|
868
|
+
await expect(replacementProvider.clientInformation()).resolves.toEqual({
|
|
869
|
+
client_id: "client-b",
|
|
870
|
+
client_secret: "secret-b",
|
|
871
|
+
redirect_uris: ["http://localhost/callback-2"],
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it.each([undefined, "registered-client"])(
|
|
876
|
+
"keeps static SDK writeback separate from dynamic client %s",
|
|
877
|
+
async (clientId) => {
|
|
878
|
+
const { storage, getState } = createStorage({
|
|
879
|
+
serverUrl,
|
|
880
|
+
discoveryState: discoveryStateFor("https://auth.example.com"),
|
|
881
|
+
});
|
|
882
|
+
const dynamicProvider = createProvider(storage);
|
|
883
|
+
if (clientId) {
|
|
884
|
+
await dynamicProvider.saveClientInformation?.({
|
|
885
|
+
client_id: clientId,
|
|
886
|
+
redirect_uris: ["http://localhost/callback"],
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const staticProvider = createStaticProvider(storage);
|
|
890
|
+
|
|
891
|
+
await expect(
|
|
892
|
+
auth(staticProvider, { serverUrl, fetchFn: rejectFetch }),
|
|
893
|
+
).resolves.toBe("REDIRECT");
|
|
894
|
+
|
|
895
|
+
expect(await dynamicProvider.clientInformation()).toEqual(
|
|
896
|
+
clientId
|
|
897
|
+
? {
|
|
898
|
+
client_id: "registered-client",
|
|
899
|
+
redirect_uris: ["http://localhost/callback"],
|
|
900
|
+
}
|
|
901
|
+
: undefined,
|
|
902
|
+
);
|
|
903
|
+
expect(
|
|
904
|
+
await createProvider(
|
|
905
|
+
createStorage(getState()).storage,
|
|
906
|
+
).clientInformation(),
|
|
907
|
+
).toEqual(await dynamicProvider.clientInformation());
|
|
908
|
+
expect(await staticProvider.clientInformation()).toMatchObject({
|
|
909
|
+
client_id: "client-a",
|
|
910
|
+
issuer: "https://auth.example.com",
|
|
911
|
+
});
|
|
912
|
+
},
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
it("drops the configured secret when the SDK re-registers at a new issuer", async () => {
|
|
916
|
+
const { storage, getState } = createStorage({
|
|
917
|
+
serverUrl,
|
|
918
|
+
discoveryState: discoveryStateFor("https://auth.example.com"),
|
|
919
|
+
});
|
|
920
|
+
const provider = createStaticProvider(storage, "client-secret");
|
|
921
|
+
|
|
922
|
+
await expect(
|
|
923
|
+
auth(provider, { serverUrl, fetchFn: rejectFetch }),
|
|
924
|
+
).resolves.toBe("REDIRECT");
|
|
925
|
+
await provider.saveDiscoveryState?.(
|
|
926
|
+
discoveryStateFor("https://moved.example.com"),
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
await expect(
|
|
930
|
+
auth(provider, {
|
|
931
|
+
serverUrl,
|
|
932
|
+
fetchFn: async () =>
|
|
933
|
+
new Response(
|
|
934
|
+
JSON.stringify({
|
|
935
|
+
client_id: "registered-client",
|
|
936
|
+
redirect_uris: ["http://localhost/callback"],
|
|
937
|
+
}),
|
|
938
|
+
{ status: 201, headers: { "content-type": "application/json" } },
|
|
939
|
+
),
|
|
940
|
+
}),
|
|
941
|
+
).resolves.toBe("REDIRECT");
|
|
942
|
+
|
|
943
|
+
expect(await provider.clientInformation()).toEqual({
|
|
944
|
+
client_id: "registered-client",
|
|
945
|
+
redirect_uris: ["http://localhost/callback"],
|
|
946
|
+
issuer: "https://moved.example.com",
|
|
947
|
+
});
|
|
948
|
+
expect(getState()?.clientInformation).toBeUndefined();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it.each(["client", "all"] as const)(
|
|
952
|
+
"restores the configured client through the %s invalidation scope",
|
|
953
|
+
async (scope) => {
|
|
954
|
+
const { storage } = createStorage({
|
|
955
|
+
serverUrl,
|
|
956
|
+
discoveryState: discoveryStateFor("https://auth.example.com"),
|
|
957
|
+
});
|
|
958
|
+
const provider = createStaticProvider(storage, "client-secret");
|
|
959
|
+
|
|
960
|
+
await expect(
|
|
961
|
+
auth(provider, { serverUrl, fetchFn: rejectFetch }),
|
|
962
|
+
).resolves.toBe("REDIRECT");
|
|
963
|
+
await provider.invalidateCredentials?.(scope);
|
|
964
|
+
|
|
965
|
+
expect(await provider.clientInformation()).toEqual({
|
|
966
|
+
client_id: "client-a",
|
|
967
|
+
client_secret: "client-secret",
|
|
968
|
+
redirect_uris: ["http://localhost/callback"],
|
|
969
|
+
});
|
|
970
|
+
},
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
it("keeps a provider built while the clear is in flight usable", async () => {
|
|
974
|
+
const { storage, getState } = createStorage();
|
|
975
|
+
let releaseClear: (() => void) | undefined;
|
|
976
|
+
const clearAuthState = storage.clearAuthState;
|
|
977
|
+
storage.clearAuthState = async (serverId) => {
|
|
978
|
+
await new Promise<void>((resolve) => {
|
|
979
|
+
releaseClear = resolve;
|
|
980
|
+
});
|
|
981
|
+
await clearAuthState(serverId);
|
|
982
|
+
};
|
|
983
|
+
const provider = createStaticProvider(storage);
|
|
984
|
+
await provider.saveTokens({
|
|
985
|
+
access_token: "access-token",
|
|
986
|
+
token_type: "bearer",
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const clear = clearOAuthProviderAuthState(storage, "docs");
|
|
990
|
+
await vi.waitFor(() => expect(releaseClear).toBeTypeOf("function"));
|
|
991
|
+
|
|
992
|
+
const replacementProvider = createProvider(storage);
|
|
993
|
+
releaseClear!();
|
|
994
|
+
await clear;
|
|
995
|
+
expect(getState()).toBeNull();
|
|
996
|
+
|
|
997
|
+
await replacementProvider.saveCodeVerifier("new-verifier");
|
|
998
|
+
expect(getState()).toEqual({ serverUrl, codeVerifier: "new-verifier" });
|
|
999
|
+
});
|
|
1000
|
+
});
|