@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.
Files changed (58) hide show
  1. package/dist/ClientAuthentication.d.ts +7 -0
  2. package/dist/Session.d.ts +174 -0
  3. package/dist/constant.d.ts +2 -2
  4. package/dist/defaultSession.d.ts +70 -0
  5. package/dist/dependencies.d.ts +12 -0
  6. package/dist/index.js +833 -34
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +833 -34
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/login/oidc/AggregateOidcHandler.d.ts +10 -0
  11. package/dist/login/oidc/AggregateRedirectHandler.d.ts +10 -0
  12. package/dist/login/oidc/ClientRegistrar.d.ts +7 -0
  13. package/dist/login/oidc/IssuerConfigFetcher.d.ts +10 -0
  14. package/dist/login/oidc/OidcLoginHandler.d.ts +10 -0
  15. package/dist/login/oidc/Redirector.d.ts +7 -0
  16. package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.d.ts +3 -0
  17. package/dist/login/oidc/incomingRedirectHandler/ErrorOidcHandler.d.ts +10 -0
  18. package/dist/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.d.ts +10 -0
  19. package/dist/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.d.ts +12 -0
  20. package/dist/login/oidc/refresh/TokenRefresher.d.ts +7 -0
  21. package/dist/logout/buildRpInitiatedLogout.d.ts +7 -0
  22. package/dist/sessionInfo/SessionInfoManager.d.ts +18 -0
  23. package/dist/solid-client-authn.bundle.js +7 -7
  24. package/dist/solid-client-authn.bundle.js.map +1 -1
  25. package/dist/storage/BrowserStorage.d.ts +7 -0
  26. package/dist/storage/StorageUtility.d.ts +14 -0
  27. package/dist/util/UuidGenerator.d.ts +6 -0
  28. package/package.json +8 -8
  29. package/rollup.config.mjs +1 -1
  30. package/src/ClientAuthentication.spec.ts +106 -57
  31. package/src/ClientAuthentication.ts +18 -7
  32. package/src/Session.spec.ts +51 -54
  33. package/src/Session.ts +24 -18
  34. package/src/__mocks__/ClientAuthentication.ts +2 -2
  35. package/src/defaultSession.ts +4 -3
  36. package/src/dependencies.ts +5 -5
  37. package/src/login/oidc/ClientRegistrar.spec.ts +22 -22
  38. package/src/login/oidc/ClientRegistrar.ts +4 -2
  39. package/src/login/oidc/IssuerConfigFetcher.spec.ts +25 -25
  40. package/src/login/oidc/IssuerConfigFetcher.ts +8 -6
  41. package/src/login/oidc/OidcLoginHandler.spec.ts +15 -15
  42. package/src/login/oidc/OidcLoginHandler.ts +14 -9
  43. package/src/login/oidc/Redirector.spec.ts +1 -1
  44. package/src/login/oidc/__mocks__/ClientRegistrar.ts +3 -3
  45. package/src/login/oidc/__mocks__/IssuerConfigFetcher.ts +3 -3
  46. package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.spec.ts +42 -42
  47. package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +19 -13
  48. package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.spec.ts +12 -12
  49. package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.ts +2 -2
  50. package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.spec.ts +7 -7
  51. package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.ts +2 -2
  52. package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.spec.ts +17 -17
  53. package/src/login/oidc/refresh/TokenRefresher.spec.ts +14 -14
  54. package/src/login/oidc/refresh/TokenRefresher.ts +12 -8
  55. package/src/login/oidc/refresh/__mocks__/TokenRefresher.ts +1 -1
  56. package/src/sessionInfo/SessionInfoManager.spec.ts +37 -15
  57. package/src/sessionInfo/SessionInfoManager.ts +46 -50
  58. 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
  }
@@ -1,6 +1,12 @@
1
+ /**
2
+ * @hidden
3
+ */
1
4
  export interface IUuidGenerator {
2
5
  v4(): string;
3
6
  }
7
+ /**
8
+ * @hidden
9
+ */
4
10
  export default class UuidGenerator {
5
11
  v4(): string;
6
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inrupt/solid-client-authn-browser",
3
- "version": "1.17.1",
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.1.0",
29
- "@types/uuid": "^9.0.1"
28
+ "@types/node": "^20.6.0",
29
+ "@types/uuid": "^9.0.4"
30
30
  },
31
31
  "dependencies": {
32
- "@inrupt/oidc-client-ext": "^1.17.1",
33
- "@inrupt/solid-client-authn-core": "^1.17.1",
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.3.7",
37
- "uuid": "^9.0.0"
36
+ "jose": "^4.14.6",
37
+ "uuid": "^9.0.1"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
- "gitHead": "0c27ce0ae0da67838526a538adc78b2f96f9b96b"
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 '../../rollup.common.mjs';
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
- // eslint-disable-next-line no-restricted-globals
312
- history.replaceState = jest.fn();
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
- // eslint-disable-next-line no-restricted-globals
378
- history.replaceState = jest.fn();
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://coolapp.com/redirect?state=userId&id_token=idToken&access_token=accessToken";
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
- // eslint-disable-next-line no-restricted-globals
412
- history.replaceState = jest.fn();
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
- // eslint-disable-next-line no-restricted-globals
428
- history.replaceState = jest.fn();
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
- // eslint-disable-next-line no-restricted-globals
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
- // eslint-disable-next-line no-restricted-globals
455
- history.replaceState = jest.fn();
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
- // eslint-disable-next-line no-restricted-globals
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
- // eslint-disable-next-line no-restricted-globals
471
- history.replaceState = jest.fn();
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
- // eslint-disable-next-line no-restricted-globals
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 or it includes a hash fragment.`
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
  }