@inrupt/solid-client-authn-browser 1.17.1 → 1.17.3
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/ClientAuthentication.d.ts +7 -0
- package/dist/Session.d.ts +174 -0
- package/dist/constant.d.ts +2 -2
- package/dist/defaultSession.d.ts +70 -0
- package/dist/dependencies.d.ts +12 -0
- package/dist/index.js +833 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +833 -34
- package/dist/index.mjs.map +1 -1
- package/dist/login/oidc/AggregateOidcHandler.d.ts +10 -0
- package/dist/login/oidc/AggregateRedirectHandler.d.ts +10 -0
- package/dist/login/oidc/ClientRegistrar.d.ts +7 -0
- package/dist/login/oidc/IssuerConfigFetcher.d.ts +10 -0
- package/dist/login/oidc/OidcLoginHandler.d.ts +10 -0
- package/dist/login/oidc/Redirector.d.ts +7 -0
- package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.d.ts +3 -0
- package/dist/login/oidc/incomingRedirectHandler/ErrorOidcHandler.d.ts +10 -0
- package/dist/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.d.ts +10 -0
- package/dist/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.d.ts +12 -0
- package/dist/login/oidc/refresh/TokenRefresher.d.ts +7 -0
- package/dist/logout/buildRpInitiatedLogout.d.ts +7 -0
- package/dist/sessionInfo/SessionInfoManager.d.ts +18 -0
- package/dist/solid-client-authn.bundle.js +7 -7
- package/dist/solid-client-authn.bundle.js.map +1 -1
- package/dist/storage/BrowserStorage.d.ts +7 -0
- package/dist/storage/StorageUtility.d.ts +14 -0
- package/dist/util/UuidGenerator.d.ts +6 -0
- package/package.json +8 -8
- package/rollup.config.mjs +1 -1
- package/src/ClientAuthentication.spec.ts +106 -57
- package/src/ClientAuthentication.ts +18 -7
- package/src/Session.spec.ts +51 -54
- package/src/Session.ts +24 -18
- package/src/__mocks__/ClientAuthentication.ts +2 -2
- package/src/defaultSession.ts +4 -3
- package/src/dependencies.ts +5 -5
- package/src/login/oidc/ClientRegistrar.spec.ts +22 -22
- package/src/login/oidc/ClientRegistrar.ts +4 -2
- package/src/login/oidc/IssuerConfigFetcher.spec.ts +25 -25
- package/src/login/oidc/IssuerConfigFetcher.ts +8 -6
- package/src/login/oidc/OidcLoginHandler.spec.ts +15 -15
- package/src/login/oidc/OidcLoginHandler.ts +14 -9
- package/src/login/oidc/Redirector.spec.ts +1 -1
- package/src/login/oidc/__mocks__/ClientRegistrar.ts +3 -3
- package/src/login/oidc/__mocks__/IssuerConfigFetcher.ts +3 -3
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.spec.ts +42 -42
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +19 -13
- package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.spec.ts +12 -12
- package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.ts +2 -2
- package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.spec.ts +7 -7
- package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.ts +2 -2
- package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.spec.ts +17 -17
- package/src/login/oidc/refresh/TokenRefresher.spec.ts +14 -14
- package/src/login/oidc/refresh/TokenRefresher.ts +12 -8
- package/src/login/oidc/refresh/__mocks__/TokenRefresher.ts +1 -1
- package/src/sessionInfo/SessionInfoManager.spec.ts +37 -15
- package/src/sessionInfo/SessionInfoManager.ts +46 -50
- package/src/sessionInfo/__mocks__/SessionInfoManager.ts +3 -3
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hidden
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
1
5
|
import type { IStorage } from "@inrupt/solid-client-authn-core";
|
|
6
|
+
/**
|
|
7
|
+
* @hidden
|
|
8
|
+
*/
|
|
2
9
|
export default class BrowserStorage implements IStorage {
|
|
3
10
|
get storage(): typeof window.localStorage;
|
|
4
11
|
get(key: string): Promise<string | undefined>;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hidden
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A helper class that will validate items taken from local storage
|
|
7
|
+
*/
|
|
1
8
|
import type { IStorage } from "@inrupt/solid-client-authn-core";
|
|
2
9
|
import { StorageUtility } from "@inrupt/solid-client-authn-core";
|
|
10
|
+
/**
|
|
11
|
+
* This class in a no-value-added extension of StorageUtility from the core module.
|
|
12
|
+
* The reason it has to be declared is for TSyringe to find the decorators in the
|
|
13
|
+
* same modules as where the dependency container is declared (in this case,
|
|
14
|
+
* the browser module, with the dependancy container in dependencies.ts).
|
|
15
|
+
* @hidden
|
|
16
|
+
*/
|
|
3
17
|
export default class StorageUtilityBrowser extends StorageUtility {
|
|
4
18
|
constructor(secureStorage: IStorage, insecureStorage: IStorage);
|
|
5
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inrupt/solid-client-authn-browser",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"browser": "dist/index.js",
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"build-docs-preview-site": "npm run build-api-docs; cd docs/api; make html"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^20.
|
|
29
|
-
"@types/uuid": "^9.0.
|
|
28
|
+
"@types/node": "^20.6.0",
|
|
29
|
+
"@types/uuid": "^9.0.4"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@inrupt/oidc-client-ext": "^1.17.
|
|
33
|
-
"@inrupt/solid-client-authn-core": "^1.17.
|
|
32
|
+
"@inrupt/oidc-client-ext": "^1.17.3",
|
|
33
|
+
"@inrupt/solid-client-authn-core": "^1.17.3",
|
|
34
34
|
"@inrupt/universal-fetch": "^1.0.2",
|
|
35
35
|
"events": "^3.3.0",
|
|
36
|
-
"jose": "^4.
|
|
37
|
-
"uuid": "^9.0.
|
|
36
|
+
"jose": "^4.14.6",
|
|
37
|
+
"uuid": "^9.0.1"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "ef10e8366ee8511f726b6cf5918f9e9c6653c6cc"
|
|
43
43
|
}
|
package/rollup.config.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from "node:module";
|
|
|
7
7
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
8
8
|
import commonjs from '@rollup/plugin-commonjs';
|
|
9
9
|
import terser from '@rollup/plugin-terser';
|
|
10
|
-
import createConfig, { createSharedConfig } from '
|
|
10
|
+
import createConfig, { createSharedConfig } from '@inrupt/base-rollup-config';
|
|
11
11
|
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
const pkg = require("./package.json");
|
|
@@ -50,7 +50,7 @@ import { mockDefaultIssuerConfigFetcher } from "./login/oidc/__mocks__/IssuerCon
|
|
|
50
50
|
|
|
51
51
|
jest.mock("@inrupt/universal-fetch", () => {
|
|
52
52
|
const fetchModule = jest.requireActual(
|
|
53
|
-
"@inrupt/universal-fetch"
|
|
53
|
+
"@inrupt/universal-fetch",
|
|
54
54
|
) as typeof UniversalFetch;
|
|
55
55
|
return {
|
|
56
56
|
...fetchModule,
|
|
@@ -60,7 +60,7 @@ jest.mock("@inrupt/universal-fetch", () => {
|
|
|
60
60
|
|
|
61
61
|
jest.mock("@inrupt/solid-client-authn-core", () => {
|
|
62
62
|
const actualCoreModule = jest.requireActual(
|
|
63
|
-
"@inrupt/solid-client-authn-core"
|
|
63
|
+
"@inrupt/solid-client-authn-core",
|
|
64
64
|
) as typeof SolidClientAuthnCore;
|
|
65
65
|
return {
|
|
66
66
|
...actualCoreModule,
|
|
@@ -78,7 +78,7 @@ const mockSessionStorage = async (
|
|
|
78
78
|
options: SessionStorageOptions = {
|
|
79
79
|
clientId: "https://some.app/registration",
|
|
80
80
|
issuer: "https://some.issuer",
|
|
81
|
-
}
|
|
81
|
+
},
|
|
82
82
|
): Promise<StorageUtility> => {
|
|
83
83
|
return new StorageUtility(
|
|
84
84
|
mockStorage({
|
|
@@ -92,7 +92,7 @@ const mockSessionStorage = async (
|
|
|
92
92
|
clientId: options.clientId,
|
|
93
93
|
issuer: options.issuer,
|
|
94
94
|
},
|
|
95
|
-
})
|
|
95
|
+
}),
|
|
96
96
|
);
|
|
97
97
|
};
|
|
98
98
|
|
|
@@ -107,14 +107,14 @@ describe("ClientAuthentication", () => {
|
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
function getClientAuthentication(
|
|
110
|
-
mocks: Partial<typeof defaultMocks> = defaultMocks
|
|
110
|
+
mocks: Partial<typeof defaultMocks> = defaultMocks,
|
|
111
111
|
): ClientAuthentication {
|
|
112
112
|
return new ClientAuthentication(
|
|
113
113
|
mocks.loginHandler ?? defaultMocks.loginHandler,
|
|
114
114
|
mocks.redirectHandler ?? defaultMocks.redirectHandler,
|
|
115
115
|
mocks.logoutHandler ?? defaultMocks.logoutHandler,
|
|
116
116
|
mocks.sessionInfoManager ?? defaultMocks.sessionInfoManager,
|
|
117
|
-
mocks.issuerConfigFetcher ?? defaultMocks.issuerConfigFetcher
|
|
117
|
+
mocks.issuerConfigFetcher ?? defaultMocks.issuerConfigFetcher,
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -144,7 +144,7 @@ describe("ClientAuthentication", () => {
|
|
|
144
144
|
clientId: "coolApp",
|
|
145
145
|
oidcIssuer: "https://idp.com",
|
|
146
146
|
},
|
|
147
|
-
mockEmitter
|
|
147
|
+
mockEmitter,
|
|
148
148
|
);
|
|
149
149
|
expect(defaultMocks.loginHandler.handle).toHaveBeenCalledWith({
|
|
150
150
|
sessionId: "mySession",
|
|
@@ -167,7 +167,7 @@ describe("ClientAuthentication", () => {
|
|
|
167
167
|
redirectUrl: "https://coolapp.com/redirect",
|
|
168
168
|
oidcIssuer: "https://idp.com",
|
|
169
169
|
},
|
|
170
|
-
mockEmitter
|
|
170
|
+
mockEmitter,
|
|
171
171
|
);
|
|
172
172
|
expect(defaultMocks.loginHandler.handle).toHaveBeenCalledWith({
|
|
173
173
|
sessionId: "mySession",
|
|
@@ -192,7 +192,7 @@ describe("ClientAuthentication", () => {
|
|
|
192
192
|
oidcIssuer: "https://idp.com",
|
|
193
193
|
tokenType: "Bearer",
|
|
194
194
|
},
|
|
195
|
-
mockEmitter
|
|
195
|
+
mockEmitter,
|
|
196
196
|
);
|
|
197
197
|
expect(defaultMocks.loginHandler.handle).toHaveBeenCalledWith({
|
|
198
198
|
sessionId: "mySession",
|
|
@@ -214,7 +214,7 @@ describe("ClientAuthentication", () => {
|
|
|
214
214
|
await nonEmptyStorage.setForUser(
|
|
215
215
|
"someUser",
|
|
216
216
|
{ someKey: "someValue" },
|
|
217
|
-
{ secure: true }
|
|
217
|
+
{ secure: true },
|
|
218
218
|
);
|
|
219
219
|
const clientAuthn = getClientAuthentication({
|
|
220
220
|
sessionInfoManager: mockSessionInfoManager(nonEmptyStorage),
|
|
@@ -228,17 +228,17 @@ describe("ClientAuthentication", () => {
|
|
|
228
228
|
redirectUrl: "https://coolapp.com/redirect",
|
|
229
229
|
oidcIssuer: "https://idp.com",
|
|
230
230
|
},
|
|
231
|
-
mockEmitter
|
|
231
|
+
mockEmitter,
|
|
232
232
|
);
|
|
233
233
|
await expect(
|
|
234
|
-
nonEmptyStorage.getForUser("someUser", "someKey", { secure: true })
|
|
234
|
+
nonEmptyStorage.getForUser("someUser", "someKey", { secure: true }),
|
|
235
235
|
).resolves.toBeUndefined();
|
|
236
236
|
await expect(
|
|
237
|
-
nonEmptyStorage.getForUser("someUser", "someKey", { secure: false })
|
|
237
|
+
nonEmptyStorage.getForUser("someUser", "someKey", { secure: false }),
|
|
238
238
|
).resolves.toBeUndefined();
|
|
239
239
|
// This test is only necessary until the key is stored safely
|
|
240
240
|
await expect(
|
|
241
|
-
nonEmptyStorage.get("clientKey", { secure: false })
|
|
241
|
+
nonEmptyStorage.get("clientKey", { secure: false }),
|
|
242
242
|
).resolves.toBeUndefined();
|
|
243
243
|
});
|
|
244
244
|
|
|
@@ -253,8 +253,8 @@ describe("ClientAuthentication", () => {
|
|
|
253
253
|
redirectUrl: "not a valid URL",
|
|
254
254
|
oidcIssuer: "https://idp.com",
|
|
255
255
|
},
|
|
256
|
-
mockEmitter
|
|
257
|
-
)
|
|
256
|
+
mockEmitter,
|
|
257
|
+
),
|
|
258
258
|
).rejects.toThrow();
|
|
259
259
|
});
|
|
260
260
|
|
|
@@ -269,11 +269,39 @@ describe("ClientAuthentication", () => {
|
|
|
269
269
|
redirectUrl: "https://example.org/redirect#some-fragment",
|
|
270
270
|
oidcIssuer: "https://idp.com",
|
|
271
271
|
},
|
|
272
|
-
mockEmitter
|
|
273
|
-
)
|
|
272
|
+
mockEmitter,
|
|
273
|
+
),
|
|
274
274
|
).rejects.toThrow("hash fragment");
|
|
275
275
|
});
|
|
276
276
|
|
|
277
|
+
it("throws if the redirect IRI contains a reserved query parameter, with a helpful message", async () => {
|
|
278
|
+
const clientAuthn = getClientAuthentication();
|
|
279
|
+
await expect(() =>
|
|
280
|
+
clientAuthn.login(
|
|
281
|
+
{
|
|
282
|
+
sessionId: "someUser",
|
|
283
|
+
tokenType: "DPoP",
|
|
284
|
+
clientId: "coolApp",
|
|
285
|
+
redirectUrl: "https://example.org/redirect?state=1234",
|
|
286
|
+
oidcIssuer: "https://idp.com",
|
|
287
|
+
},
|
|
288
|
+
mockEmitter,
|
|
289
|
+
),
|
|
290
|
+
).rejects.toThrow("query parameter");
|
|
291
|
+
await expect(() =>
|
|
292
|
+
clientAuthn.login(
|
|
293
|
+
{
|
|
294
|
+
sessionId: "someUser",
|
|
295
|
+
tokenType: "DPoP",
|
|
296
|
+
clientId: "coolApp",
|
|
297
|
+
redirectUrl: "https://example.org/redirect?code=1234",
|
|
298
|
+
oidcIssuer: "https://idp.com",
|
|
299
|
+
},
|
|
300
|
+
mockEmitter,
|
|
301
|
+
),
|
|
302
|
+
).rejects.toThrow("query parameter");
|
|
303
|
+
});
|
|
304
|
+
|
|
277
305
|
it("does not normalize the redirect URL if provided by the user", async () => {
|
|
278
306
|
const clientAuthn = getClientAuthentication();
|
|
279
307
|
await clientAuthn.login(
|
|
@@ -285,12 +313,12 @@ describe("ClientAuthentication", () => {
|
|
|
285
313
|
oidcIssuer: "https://idp.com",
|
|
286
314
|
tokenType: "Bearer",
|
|
287
315
|
},
|
|
288
|
-
mockEmitter
|
|
316
|
+
mockEmitter,
|
|
289
317
|
);
|
|
290
318
|
expect(defaultMocks.loginHandler.handle).toHaveBeenCalledWith(
|
|
291
319
|
expect.objectContaining({
|
|
292
320
|
redirectUrl: "https://example.org",
|
|
293
|
-
})
|
|
321
|
+
}),
|
|
294
322
|
);
|
|
295
323
|
});
|
|
296
324
|
});
|
|
@@ -308,8 +336,12 @@ describe("ClientAuthentication", () => {
|
|
|
308
336
|
// TODO: add tests for events & errors
|
|
309
337
|
|
|
310
338
|
it("reverts back to un-authenticated fetch on logout", async () => {
|
|
311
|
-
|
|
312
|
-
|
|
339
|
+
window.history.replaceState = jest
|
|
340
|
+
.fn<typeof window.history.replaceState>()
|
|
341
|
+
.mockImplementationOnce((_data, _unused, url) => {
|
|
342
|
+
// Pretend the current location is updated
|
|
343
|
+
window.location.href = url as string;
|
|
344
|
+
});
|
|
313
345
|
const clientAuthn = getClientAuthentication();
|
|
314
346
|
|
|
315
347
|
const unauthFetch = clientAuthn.fetch;
|
|
@@ -337,7 +369,7 @@ describe("ClientAuthentication", () => {
|
|
|
337
369
|
it("creates a session for the global user", async () => {
|
|
338
370
|
const clientAuthn = getClientAuthentication();
|
|
339
371
|
await expect(() => clientAuthn.getAllSessionInfo()).rejects.toThrow(
|
|
340
|
-
"Not implemented"
|
|
372
|
+
"Not implemented",
|
|
341
373
|
);
|
|
342
374
|
});
|
|
343
375
|
});
|
|
@@ -355,8 +387,8 @@ describe("ClientAuthentication", () => {
|
|
|
355
387
|
{
|
|
356
388
|
"solidClientAuthenticationUser:mySession": { ...sessionInfo },
|
|
357
389
|
},
|
|
358
|
-
true
|
|
359
|
-
)
|
|
390
|
+
true,
|
|
391
|
+
),
|
|
360
392
|
),
|
|
361
393
|
});
|
|
362
394
|
const session = await clientAuthn.getSessionInfo("mySession");
|
|
@@ -374,16 +406,20 @@ describe("ClientAuthentication", () => {
|
|
|
374
406
|
mockEmitter.emit = jest.fn<typeof mockEmitter.emit>();
|
|
375
407
|
|
|
376
408
|
it("calls handle redirect", async () => {
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
window.history.replaceState = jest
|
|
410
|
+
.fn<typeof window.history.replaceState>()
|
|
411
|
+
.mockImplementationOnce((_data, _unused, url) => {
|
|
412
|
+
// Pretend the current location is updated
|
|
413
|
+
window.location.href = url as string;
|
|
414
|
+
});
|
|
379
415
|
const expectedResult = SessionCreatorCreateResponse;
|
|
380
416
|
const clientAuthn = getClientAuthentication();
|
|
381
417
|
const unauthFetch = clientAuthn.fetch;
|
|
382
418
|
const url =
|
|
383
|
-
"https://
|
|
419
|
+
"https://example.org/redirect?state=userId&id_token=idToken&access_token=accessToken";
|
|
384
420
|
const redirectInfo = await clientAuthn.handleIncomingRedirect(
|
|
385
421
|
url,
|
|
386
|
-
mockEmitter
|
|
422
|
+
mockEmitter,
|
|
387
423
|
);
|
|
388
424
|
|
|
389
425
|
// Our injected mocked response may also contain internal-only data (for
|
|
@@ -395,11 +431,11 @@ describe("ClientAuthentication", () => {
|
|
|
395
431
|
expect(redirectInfo?.webId).toEqual(expectedResult.webId);
|
|
396
432
|
expect(redirectInfo?.isLoggedIn).toEqual(expectedResult.isLoggedIn);
|
|
397
433
|
expect(redirectInfo?.expirationDate).toEqual(
|
|
398
|
-
expectedResult.expirationDate
|
|
434
|
+
expectedResult.expirationDate,
|
|
399
435
|
);
|
|
400
436
|
expect(defaultMocks.redirectHandler.handle).toHaveBeenCalledWith(
|
|
401
437
|
url,
|
|
402
|
-
mockEmitter
|
|
438
|
+
mockEmitter,
|
|
403
439
|
);
|
|
404
440
|
|
|
405
441
|
// Calling the redirect handler should have updated the fetch.
|
|
@@ -408,8 +444,12 @@ describe("ClientAuthentication", () => {
|
|
|
408
444
|
});
|
|
409
445
|
|
|
410
446
|
it("clears the current IRI from OAuth query parameters in the auth code flow", async () => {
|
|
411
|
-
|
|
412
|
-
|
|
447
|
+
window.history.replaceState = jest
|
|
448
|
+
.fn<typeof window.history.replaceState>()
|
|
449
|
+
.mockImplementationOnce((_data, _unused, url) => {
|
|
450
|
+
// Pretend the current location is updated
|
|
451
|
+
window.location.href = url as string;
|
|
452
|
+
});
|
|
413
453
|
const clientAuthn = getClientAuthentication();
|
|
414
454
|
const url =
|
|
415
455
|
"https://coolapp.com/redirect?state=someState&code=someAuthCode&iss=someIssuer";
|
|
@@ -418,17 +458,21 @@ describe("ClientAuthentication", () => {
|
|
|
418
458
|
expect(history.replaceState).toHaveBeenCalledWith(
|
|
419
459
|
null,
|
|
420
460
|
"",
|
|
421
|
-
"https://coolapp.com/redirect"
|
|
461
|
+
"https://coolapp.com/redirect",
|
|
422
462
|
);
|
|
423
463
|
expect(mockEmitter.emit).not.toHaveBeenCalled();
|
|
424
464
|
});
|
|
425
465
|
|
|
426
466
|
it("clears the current IRI from OAuth query parameters even if auth flow fails", async () => {
|
|
427
|
-
|
|
428
|
-
|
|
467
|
+
window.history.replaceState = jest
|
|
468
|
+
.fn<typeof window.history.replaceState>()
|
|
469
|
+
.mockImplementationOnce((_data, _unused, url) => {
|
|
470
|
+
// Pretend the current location is updated
|
|
471
|
+
window.location.href = url as string;
|
|
472
|
+
});
|
|
429
473
|
|
|
430
474
|
mockHandleIncomingRedirect.mockImplementationOnce(() =>
|
|
431
|
-
Promise.reject(new Error("Something went wrong"))
|
|
475
|
+
Promise.reject(new Error("Something went wrong")),
|
|
432
476
|
);
|
|
433
477
|
|
|
434
478
|
const clientAuthn = getClientAuthentication();
|
|
@@ -436,48 +480,53 @@ describe("ClientAuthentication", () => {
|
|
|
436
480
|
const url =
|
|
437
481
|
"https://coolapp.com/redirect?state=someState&code=someAuthCode";
|
|
438
482
|
await clientAuthn.handleIncomingRedirect(url, mockEmitter);
|
|
439
|
-
|
|
440
|
-
expect(history.replaceState).toHaveBeenCalledWith(
|
|
483
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
441
484
|
null,
|
|
442
485
|
"",
|
|
443
|
-
"https://coolapp.com/redirect"
|
|
486
|
+
"https://coolapp.com/redirect",
|
|
444
487
|
);
|
|
445
488
|
|
|
446
489
|
expect(mockEmitter.emit).toHaveBeenCalledWith(
|
|
447
490
|
EVENTS.ERROR,
|
|
448
491
|
"redirect",
|
|
449
|
-
new Error("Something went wrong")
|
|
492
|
+
new Error("Something went wrong"),
|
|
450
493
|
);
|
|
451
494
|
});
|
|
452
495
|
|
|
453
496
|
it("clears the current IRI from OAuth query parameters in the implicit flow", async () => {
|
|
454
|
-
|
|
455
|
-
|
|
497
|
+
window.history.replaceState = jest
|
|
498
|
+
.fn<typeof window.history.replaceState>()
|
|
499
|
+
.mockImplementationOnce((_data, _unused, url) => {
|
|
500
|
+
// Pretend the current location is updated
|
|
501
|
+
window.location.href = url as string;
|
|
502
|
+
});
|
|
456
503
|
const clientAuthn = getClientAuthentication();
|
|
457
504
|
const url =
|
|
458
505
|
"https://coolapp.com/redirect?state=someState&id_token=idToken&access_token=accessToken";
|
|
459
506
|
await clientAuthn.handleIncomingRedirect(url, mockEmitter);
|
|
460
|
-
|
|
461
|
-
expect(history.replaceState).toHaveBeenCalledWith(
|
|
507
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
462
508
|
null,
|
|
463
509
|
"",
|
|
464
|
-
"https://coolapp.com/redirect"
|
|
510
|
+
"https://coolapp.com/redirect",
|
|
465
511
|
);
|
|
466
512
|
expect(mockEmitter.emit).not.toHaveBeenCalled();
|
|
467
513
|
});
|
|
468
514
|
|
|
469
515
|
it("preserves non-OAuth query strings", async () => {
|
|
470
|
-
|
|
471
|
-
|
|
516
|
+
window.history.replaceState = jest
|
|
517
|
+
.fn<typeof window.history.replaceState>()
|
|
518
|
+
.mockImplementationOnce((_data, _unused, url) => {
|
|
519
|
+
// Pretend the current location is updated
|
|
520
|
+
window.location.href = url as string;
|
|
521
|
+
});
|
|
472
522
|
const clientAuthn = getClientAuthentication();
|
|
473
523
|
const url =
|
|
474
524
|
"https://coolapp.com/redirect?state=someState&code=someAuthCode&someQuery=someValue";
|
|
475
525
|
await clientAuthn.handleIncomingRedirect(url, mockEmitter);
|
|
476
|
-
|
|
477
|
-
expect(history.replaceState).toHaveBeenCalledWith(
|
|
526
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
478
527
|
null,
|
|
479
528
|
"",
|
|
480
|
-
"https://coolapp.com/redirect?someQuery=someValue"
|
|
529
|
+
"https://coolapp.com/redirect?someQuery=someValue",
|
|
481
530
|
);
|
|
482
531
|
expect(mockEmitter.emit).not.toHaveBeenCalled();
|
|
483
532
|
});
|
|
@@ -497,14 +546,14 @@ describe("ClientAuthentication", () => {
|
|
|
497
546
|
[`${USER_SESSION_PREFIX}:${sessionId}`]: {
|
|
498
547
|
clientId: "https://some.app/registration",
|
|
499
548
|
},
|
|
500
|
-
})
|
|
549
|
+
}),
|
|
501
550
|
);
|
|
502
551
|
const clientAuthn = getClientAuthentication({
|
|
503
552
|
sessionInfoManager: mockSessionInfoManager(mockedStorage),
|
|
504
553
|
});
|
|
505
554
|
|
|
506
555
|
await expect(
|
|
507
|
-
clientAuthn.validateCurrentSession(sessionId)
|
|
556
|
+
clientAuthn.validateCurrentSession(sessionId),
|
|
508
557
|
).resolves.toBeNull();
|
|
509
558
|
});
|
|
510
559
|
|
|
@@ -519,14 +568,14 @@ describe("ClientAuthentication", () => {
|
|
|
519
568
|
}),
|
|
520
569
|
mockStorage({
|
|
521
570
|
[`${USER_SESSION_PREFIX}:${sessionId}`]: {},
|
|
522
|
-
})
|
|
571
|
+
}),
|
|
523
572
|
);
|
|
524
573
|
const clientAuthn = getClientAuthentication({
|
|
525
574
|
sessionInfoManager: mockSessionInfoManager(mockedStorage),
|
|
526
575
|
});
|
|
527
576
|
|
|
528
577
|
await expect(
|
|
529
|
-
clientAuthn.validateCurrentSession(sessionId)
|
|
578
|
+
clientAuthn.validateCurrentSession(sessionId),
|
|
530
579
|
).resolves.toBeNull();
|
|
531
580
|
});
|
|
532
581
|
|
|
@@ -542,14 +591,14 @@ describe("ClientAuthentication", () => {
|
|
|
542
591
|
});
|
|
543
592
|
|
|
544
593
|
await expect(
|
|
545
|
-
clientAuthn.validateCurrentSession(sessionId)
|
|
594
|
+
clientAuthn.validateCurrentSession(sessionId),
|
|
546
595
|
).resolves.toStrictEqual(
|
|
547
596
|
expect.objectContaining({
|
|
548
597
|
issuer: "https://some.issuer",
|
|
549
598
|
clientAppId: "https://some.app/registration",
|
|
550
599
|
sessionId,
|
|
551
600
|
webId: "https://my.pod/profile#me",
|
|
552
|
-
})
|
|
601
|
+
}),
|
|
553
602
|
);
|
|
554
603
|
});
|
|
555
604
|
});
|
|
@@ -45,7 +45,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
45
45
|
// Isn't Javascript fun?
|
|
46
46
|
login = async (
|
|
47
47
|
options: ILoginOptions,
|
|
48
|
-
eventEmitter: EventEmitter
|
|
48
|
+
eventEmitter: EventEmitter,
|
|
49
49
|
): Promise<void> => {
|
|
50
50
|
// In order to get a clean start, make sure that the session is logged out
|
|
51
51
|
// on login.
|
|
@@ -65,7 +65,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
65
65
|
options.redirectUrl ?? removeOidcQueryParam(window.location.href);
|
|
66
66
|
if (!isValidRedirectUrl(redirectUrl)) {
|
|
67
67
|
throw new Error(
|
|
68
|
-
`${redirectUrl} is not a valid redirect URL, it is either a malformed IRI
|
|
68
|
+
`${redirectUrl} is not a valid redirect URL, it is either a malformed IRI, includes a hash fragment, or reserved query parameters ('code' or 'state').`,
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
await this.loginHandler.handle({
|
|
@@ -82,7 +82,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
82
82
|
// Note that the ID token is not stored, which means the session information
|
|
83
83
|
// cannot be validated at this point.
|
|
84
84
|
validateCurrentSession = async (
|
|
85
|
-
currentSessionId: string
|
|
85
|
+
currentSessionId: string,
|
|
86
86
|
): Promise<(ISessionInfo & ISessionInternalInfo) | null> => {
|
|
87
87
|
const sessionInfo = await this.sessionInfoManager.get(currentSessionId);
|
|
88
88
|
if (
|
|
@@ -97,7 +97,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
97
97
|
|
|
98
98
|
handleIncomingRedirect = async (
|
|
99
99
|
url: string,
|
|
100
|
-
eventEmitter: EventEmitter
|
|
100
|
+
eventEmitter: EventEmitter,
|
|
101
101
|
): Promise<ISessionInfo | undefined> => {
|
|
102
102
|
try {
|
|
103
103
|
const redirectInfo = await this.redirectHandler.handle(url, eventEmitter);
|
|
@@ -109,7 +109,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
109
109
|
this.boundLogout = redirectInfo.getLogoutUrl;
|
|
110
110
|
|
|
111
111
|
// Strip the oauth params:
|
|
112
|
-
this.cleanUrlAfterRedirect(url);
|
|
112
|
+
await this.cleanUrlAfterRedirect(url);
|
|
113
113
|
|
|
114
114
|
return {
|
|
115
115
|
isLoggedIn: redirectInfo.isLoggedIn,
|
|
@@ -119,7 +119,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
119
119
|
};
|
|
120
120
|
} catch (err) {
|
|
121
121
|
// Strip the oauth params:
|
|
122
|
-
this.cleanUrlAfterRedirect(url);
|
|
122
|
+
await this.cleanUrlAfterRedirect(url);
|
|
123
123
|
|
|
124
124
|
// FIXME: EVENTS.ERROR should be errorCode, errorDescription
|
|
125
125
|
//
|
|
@@ -132,7 +132,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
private cleanUrlAfterRedirect(url: string): void {
|
|
135
|
+
private async cleanUrlAfterRedirect(url: string): Promise<void> {
|
|
136
136
|
const cleanedUpUrl = new URL(url);
|
|
137
137
|
cleanedUpUrl.searchParams.delete("state");
|
|
138
138
|
// For auth code flow
|
|
@@ -151,5 +151,16 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
|
|
|
151
151
|
// authentication library will be called again with what are now invalid
|
|
152
152
|
// query parameters!).
|
|
153
153
|
window.history.replaceState(null, "", cleanedUpUrl.toString());
|
|
154
|
+
while (window.location.href !== cleanedUpUrl.href) {
|
|
155
|
+
// Poll the current URL every ms. Active polling is required because
|
|
156
|
+
// window.history.replaceState is asynchronous, but the associated
|
|
157
|
+
// 'popstate' event which should be listened to is only sent on active
|
|
158
|
+
// navigation, which we will not have here.
|
|
159
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent
|
|
160
|
+
// eslint-disable-next-line no-await-in-loop
|
|
161
|
+
await new Promise<void>((resolve) => {
|
|
162
|
+
setTimeout(() => resolve(), 1);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
154
165
|
}
|
|
155
166
|
}
|