@inrupt/solid-client-authn-browser 3.1.1 → 5.0.0

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 (65) hide show
  1. package/README.md +9 -13
  2. package/dist/index.js +28 -38
  3. package/dist/index.js.map +1 -1
  4. package/dist/index.mjs +28 -38
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/solid-client-authn.bundle.js +1 -31
  7. package/dist/solid-client-authn.bundle.js.map +1 -1
  8. package/package.json +7 -7
  9. package/rollup.config.mjs +0 -2
  10. package/src/ClientAuthentication.spec.ts +147 -23
  11. package/src/ClientAuthentication.ts +18 -4
  12. package/src/Session.spec.ts +74 -39
  13. package/src/Session.ts +0 -1
  14. package/src/__mocks__/ClientAuthentication.ts +1 -1
  15. package/src/constant.ts +0 -1
  16. package/src/defaultSession.spec.ts +0 -1
  17. package/src/defaultSession.ts +1 -2
  18. package/src/dependencies.node.spec.ts +0 -1
  19. package/src/dependencies.spec.ts +0 -1
  20. package/src/dependencies.ts +0 -1
  21. package/src/index.browser.spec.ts +0 -1
  22. package/src/index.browser.ts +0 -1
  23. package/src/index.spec.ts +0 -1
  24. package/src/index.ts +0 -1
  25. package/src/login/__mocks__/LoginHandler.ts +0 -1
  26. package/src/login/oidc/AggregateOidcHandler.spec.ts +1 -2
  27. package/src/login/oidc/AggregateOidcHandler.ts +0 -1
  28. package/src/login/oidc/AggregateRedirectHandler.ts +0 -1
  29. package/src/login/oidc/ClientRegistrar.spec.ts +1 -1
  30. package/src/login/oidc/ClientRegistrar.ts +0 -1
  31. package/src/login/oidc/IssuerConfigFetcher.spec.ts +7 -5
  32. package/src/login/oidc/IssuerConfigFetcher.ts +3 -3
  33. package/src/login/oidc/OidcLoginHandler.spec.ts +1 -1
  34. package/src/login/oidc/OidcLoginHandler.ts +0 -1
  35. package/src/login/oidc/Redirector.spec.ts +5 -23
  36. package/src/login/oidc/Redirector.ts +0 -1
  37. package/src/login/oidc/__mocks__/ClientRegistrar.ts +0 -1
  38. package/src/login/oidc/__mocks__/IOidcHandler.ts +1 -3
  39. package/src/login/oidc/__mocks__/IOidcOptions.ts +0 -1
  40. package/src/login/oidc/__mocks__/IssuerConfigFetcher.ts +0 -2
  41. package/src/login/oidc/__mocks__/Redirector.ts +0 -1
  42. package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.spec.ts +3 -4
  43. package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +0 -1
  44. package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.spec.ts +0 -1
  45. package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.ts +0 -2
  46. package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.spec.ts +0 -1
  47. package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.ts +1 -2
  48. package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.spec.ts +1 -4
  49. package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.ts +10 -11
  50. package/src/login/oidc/oidcHandlers/OidcHandlerCanHandleTest.ts +0 -1
  51. package/src/login/oidc/refresh/TokenRefresher.spec.ts +3 -4
  52. package/src/login/oidc/refresh/TokenRefresher.ts +0 -2
  53. package/src/login/oidc/refresh/__mocks__/TokenRefresher.ts +0 -2
  54. package/src/logout/buildRpInitiatedLogout.ts +1 -1
  55. package/src/sessionInfo/SessionInfoManager.spec.ts +62 -1
  56. package/src/sessionInfo/SessionInfoManager.ts +6 -1
  57. package/src/sessionInfo/__mocks__/SessionInfoManager.ts +1 -2
  58. package/src/storage/BrowserStorage.spec.ts +0 -1
  59. package/src/storage/BrowserStorage.ts +0 -1
  60. package/src/storage/StorageUtility.ts +0 -1
  61. package/src/storage/__mocks__/LocalStorage.ts +0 -1
  62. package/src/util/UUIDGenerator.spec.ts +0 -1
  63. package/src/util/UuidGenerator.ts +0 -2
  64. package/src/util/__mocks__/UuidGenerator.ts +0 -1
  65. package/typedoc.config.mjs +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inrupt/solid-client-authn-browser",
3
- "version": "3.1.1",
3
+ "version": "5.0.0",
4
4
  "license": "MIT",
5
5
  "types": "dist/index.d.ts",
6
6
  "browser": "dist/index.js",
@@ -26,15 +26,15 @@
26
26
  "build-docs-preview-site": "npm run build-api-docs; cd docs/api; make html"
27
27
  },
28
28
  "devDependencies": {
29
- "@types/node": "^24.9.1",
30
- "@types/uuid": "^10.0.0"
29
+ "@types/node": "^25.9.1",
30
+ "@types/uuid": "^11.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@inrupt/oidc-client-ext": "^3.1.1",
34
- "@inrupt/solid-client-authn-core": "^3.1.1",
33
+ "@inrupt/oidc-client-ext": "^5.0.0",
34
+ "@inrupt/solid-client-authn-core": "^5.0.0",
35
35
  "events": "^3.3.0",
36
- "jose": "^5.1.3",
37
- "uuid": "^11.1.0"
36
+ "jose": "^6.2.3",
37
+ "uuid": "^14.0.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
package/rollup.config.mjs CHANGED
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -23,7 +22,6 @@ import { nodeResolve } from "@rollup/plugin-node-resolve";
23
22
  import commonjs from "@rollup/plugin-commonjs";
24
23
  import terser from "@rollup/plugin-terser";
25
24
  import createConfig, { createSharedConfig } from "@inrupt/base-rollup-config";
26
- // eslint-disable-next-line import/extensions
27
25
  import pkg from "./package.json" with { type: "json" };
28
26
 
29
27
  export default [
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -23,6 +22,8 @@ import { jest, it, describe, expect, beforeEach } from "@jest/globals";
23
22
  import { EventEmitter } from "events";
24
23
  import type * as SolidClientAuthnCore from "@inrupt/solid-client-authn-core";
25
24
 
25
+ declare const __setTestUrl: (url: string) => void;
26
+
26
27
  import {
27
28
  StorageUtility,
28
29
  USER_SESSION_PREFIX,
@@ -35,6 +36,7 @@ import {
35
36
  mockIncomingRedirectHandler,
36
37
  mockHandleIncomingRedirect,
37
38
  mockLogoutHandler,
39
+ // eslint-disable-next-line import/no-unresolved
38
40
  } from "@inrupt/solid-client-authn-core/mocks";
39
41
 
40
42
  import { mockLoginHandler } from "./login/__mocks__/LoginHandler";
@@ -108,11 +110,7 @@ describe("ClientAuthentication", () => {
108
110
 
109
111
  beforeEach(() => {
110
112
  jest.clearAllMocks();
111
-
112
- Object.defineProperty(window, "location", {
113
- writable: true,
114
- value: { assign: jest.fn() },
115
- });
113
+ __setTestUrl("http://localhost/");
116
114
  });
117
115
 
118
116
  describe("login", () => {
@@ -122,7 +120,7 @@ describe("ClientAuthentication", () => {
122
120
  it("calls login, and uses the window.location.href for the redirect if no redirectUrl is set", async () => {
123
121
  // Set a current window location which will be the expected URL to be
124
122
  // redirected back to, since we don't pass redirectUrl:
125
- window.location.href = "https://coolapp.test/some/redirect";
123
+ __setTestUrl("https://coolapp.test/some/redirect");
126
124
 
127
125
  const clientAuthn = getClientAuthentication();
128
126
  await clientAuthn.login(
@@ -353,11 +351,11 @@ describe("ClientAuthentication", () => {
353
351
  // TODO: add tests for events & errors
354
352
 
355
353
  it("reverts back to un-authenticated fetch on logout", async () => {
356
- window.history.replaceState = jest
357
- .fn<typeof window.history.replaceState>()
354
+ jest
355
+ .spyOn(window.history, "replaceState")
358
356
  .mockImplementationOnce((_data, _unused, url) => {
359
357
  // Pretend the current location is updated
360
- window.location.href = url as string;
358
+ __setTestUrl(url as string);
361
359
  });
362
360
  const clientAuthn = getClientAuthentication();
363
361
 
@@ -423,11 +421,11 @@ describe("ClientAuthentication", () => {
423
421
  mockEmitter.emit = jest.fn<typeof mockEmitter.emit>();
424
422
 
425
423
  it("calls handle redirect", async () => {
426
- window.history.replaceState = jest
427
- .fn<typeof window.history.replaceState>()
424
+ jest
425
+ .spyOn(window.history, "replaceState")
428
426
  .mockImplementationOnce((_data, _unused, url) => {
429
427
  // Pretend the current location is updated
430
- window.location.href = url as string;
428
+ __setTestUrl(url as string);
431
429
  });
432
430
  const expectedResult = SessionCreatorCreateResponse;
433
431
  const clientAuthn = getClientAuthentication();
@@ -462,17 +460,17 @@ describe("ClientAuthentication", () => {
462
460
  });
463
461
 
464
462
  it("clears the current IRI from OAuth query parameters in the auth code flow", async () => {
465
- window.history.replaceState = jest
466
- .fn<typeof window.history.replaceState>()
463
+ jest
464
+ .spyOn(window.history, "replaceState")
467
465
  .mockImplementationOnce((_data, _unused, url) => {
468
466
  // Pretend the current location is updated
469
- window.location.href = url as string;
467
+ __setTestUrl(url as string);
470
468
  });
471
469
  const clientAuthn = getClientAuthentication();
472
470
  const url =
473
471
  "https://coolapp.com/redirect?state=someState&code=someAuthCode&iss=someIssuer";
474
472
  await clientAuthn.handleIncomingRedirect(url, mockEmitter);
475
- // eslint-disable-next-line no-restricted-globals
473
+
476
474
  expect(history.replaceState).toHaveBeenCalledWith(
477
475
  null,
478
476
  "",
@@ -482,11 +480,11 @@ describe("ClientAuthentication", () => {
482
480
  });
483
481
 
484
482
  it("clears the current IRI from OAuth query parameters even if auth flow fails", async () => {
485
- window.history.replaceState = jest
486
- .fn<typeof window.history.replaceState>()
483
+ jest
484
+ .spyOn(window.history, "replaceState")
487
485
  .mockImplementationOnce((_data, _unused, url) => {
488
486
  // Pretend the current location is updated
489
- window.location.href = url as string;
487
+ __setTestUrl(url as string);
490
488
  });
491
489
 
492
490
  mockHandleIncomingRedirect.mockImplementationOnce(() =>
@@ -512,11 +510,11 @@ describe("ClientAuthentication", () => {
512
510
  });
513
511
 
514
512
  it("preserves non-OAuth query strings", async () => {
515
- window.history.replaceState = jest
516
- .fn<typeof window.history.replaceState>()
513
+ jest
514
+ .spyOn(window.history, "replaceState")
517
515
  .mockImplementationOnce((_data, _unused, url) => {
518
516
  // Pretend the current location is updated
519
- window.location.href = url as string;
517
+ __setTestUrl(url as string);
520
518
  });
521
519
  const clientAuthn = getClientAuthentication();
522
520
  const url =
@@ -601,4 +599,130 @@ describe("ClientAuthentication", () => {
601
599
  );
602
600
  });
603
601
  });
602
+
603
+ describe("validateCurrentSession", () => {
604
+ it("returns clientExpiresAt when expiresAt is in storage", async () => {
605
+ const sessionId = "mySession";
606
+ const expiresAt = Math.floor(Date.now() / 1000) + 10000;
607
+ const mockedStorage = new StorageUtility(
608
+ mockStorage({
609
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
610
+ isLoggedIn: "true",
611
+ webId: "https://my.pod/profile#me",
612
+ },
613
+ }),
614
+ mockStorage({
615
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
616
+ clientId: "https://some.app/registration",
617
+ clientSecret: "some-secret",
618
+ issuer: "https://some.issuer",
619
+ expiresAt: String(expiresAt),
620
+ },
621
+ }),
622
+ );
623
+ const clientAuthn = getClientAuthentication({
624
+ sessionInfoManager: mockSessionInfoManager(mockedStorage),
625
+ });
626
+
627
+ const result = await clientAuthn.validateCurrentSession(sessionId);
628
+ expect(result).toStrictEqual(
629
+ expect.objectContaining({
630
+ clientExpiresAt: expiresAt,
631
+ }),
632
+ );
633
+ });
634
+
635
+ it("returns null when client has expired", async () => {
636
+ const sessionId = "mySession";
637
+ const expiredAt = Math.floor(Date.now() / 1000) - 1000;
638
+ const mockedStorage = new StorageUtility(
639
+ mockStorage({
640
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
641
+ isLoggedIn: "true",
642
+ webId: "https://my.pod/profile#me",
643
+ },
644
+ }),
645
+ mockStorage({
646
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
647
+ clientId: "https://some.app/registration",
648
+ clientSecret: "some-secret",
649
+ issuer: "https://some.issuer",
650
+ expiresAt: String(expiredAt),
651
+ },
652
+ }),
653
+ );
654
+ const clientAuthn = getClientAuthentication({
655
+ sessionInfoManager: mockSessionInfoManager(mockedStorage),
656
+ });
657
+
658
+ await expect(
659
+ clientAuthn.validateCurrentSession(sessionId),
660
+ ).resolves.toBeNull();
661
+ });
662
+
663
+ it("returns session when client has not expired", async () => {
664
+ const sessionId = "mySession";
665
+ const futureExpiry = Math.floor(Date.now() / 1000) + 10000;
666
+ const mockedStorage = new StorageUtility(
667
+ mockStorage({
668
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
669
+ isLoggedIn: "true",
670
+ webId: "https://my.pod/profile#me",
671
+ },
672
+ }),
673
+ mockStorage({
674
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
675
+ clientId: "https://some.app/registration",
676
+ clientSecret: "some-secret",
677
+ issuer: "https://some.issuer",
678
+ expiresAt: String(futureExpiry),
679
+ },
680
+ }),
681
+ );
682
+ const clientAuthn = getClientAuthentication({
683
+ sessionInfoManager: mockSessionInfoManager(mockedStorage),
684
+ });
685
+
686
+ const result = await clientAuthn.validateCurrentSession(sessionId);
687
+ expect(result).not.toBeNull();
688
+ expect(result).toStrictEqual(
689
+ expect.objectContaining({
690
+ clientAppId: "https://some.app/registration",
691
+ issuer: "https://some.issuer",
692
+ }),
693
+ );
694
+ });
695
+
696
+ it("returns session when clientExpiresAt is 0 (never expires per OIDC DCR spec)", async () => {
697
+ const sessionId = "mySession";
698
+ const mockedStorage = new StorageUtility(
699
+ mockStorage({
700
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
701
+ isLoggedIn: "true",
702
+ webId: "https://my.pod/profile#me",
703
+ },
704
+ }),
705
+ mockStorage({
706
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
707
+ clientId: "https://some.app/registration",
708
+ clientSecret: "some-secret",
709
+ issuer: "https://some.issuer",
710
+ expiresAt: "0",
711
+ },
712
+ }),
713
+ );
714
+ const clientAuthn = getClientAuthentication({
715
+ sessionInfoManager: mockSessionInfoManager(mockedStorage),
716
+ });
717
+
718
+ const result = await clientAuthn.validateCurrentSession(sessionId);
719
+ expect(result).not.toBeNull();
720
+ expect(result).toStrictEqual(
721
+ expect.objectContaining({
722
+ clientAppId: "https://some.app/registration",
723
+ clientExpiresAt: 0,
724
+ }),
725
+ );
726
+ });
727
+ });
604
728
  });
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -38,6 +37,20 @@ import {
38
37
  import { normalizeCallbackUrl } from "@inrupt/oidc-client-ext";
39
38
  import type { EventEmitter } from "events";
40
39
 
40
+ /**
41
+ * Checks if a client's registration has expired.
42
+ */
43
+ function isClientExpired(sessionInfo: { clientExpiresAt?: number }): boolean {
44
+ // clientExpiresAt === 0 means the client never expires (per OIDC DCR spec)
45
+ if (
46
+ sessionInfo.clientExpiresAt === undefined ||
47
+ sessionInfo.clientExpiresAt === 0
48
+ ) {
49
+ return false;
50
+ }
51
+ return sessionInfo.clientExpiresAt < Math.floor(Date.now() / 1000);
52
+ }
53
+
41
54
  /**
42
55
  * @hidden
43
56
  */
@@ -78,7 +91,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
78
91
  };
79
92
 
80
93
  // Collects session information from storage, and returns them. Returns null
81
- // if the expected information cannot be found.
94
+ // if the expected information cannot be found or if the client has expired.
82
95
  // Note that the ID token is not stored, which means the session information
83
96
  // cannot be validated at this point.
84
97
  validateCurrentSession = async (
@@ -88,7 +101,8 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
88
101
  if (
89
102
  sessionInfo === undefined ||
90
103
  sessionInfo.clientAppId === undefined ||
91
- sessionInfo.issuer === undefined
104
+ sessionInfo.issuer === undefined ||
105
+ isClientExpired(sessionInfo)
92
106
  ) {
93
107
  return null;
94
108
  }
@@ -152,7 +166,7 @@ export default class ClientAuthentication extends ClientAuthenticationBase {
152
166
  // 'popstate' event which should be listened to is only sent on active
153
167
  // navigation, which we will not have here.
154
168
  // See https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#when_popstate_is_sent
155
- // eslint-disable-next-line no-await-in-loop
169
+
156
170
  await new Promise<void>((resolve) => {
157
171
  setTimeout(() => resolve(), 1);
158
172
  });
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -26,6 +25,7 @@ import {
26
25
  StorageUtility,
27
26
  USER_SESSION_PREFIX,
28
27
  } from "@inrupt/solid-client-authn-core";
28
+ // eslint-disable-next-line import/no-unresolved
29
29
  import { mockStorage } from "@inrupt/solid-client-authn-core/mocks";
30
30
  import { EventEmitter } from "stream";
31
31
  import { mockClientAuthentication } from "./__mocks__/ClientAuthentication";
@@ -46,27 +46,11 @@ const mockLocalStorage = (stored: Record<string, string>) => {
46
46
  });
47
47
  };
48
48
 
49
+ declare const __setTestUrl: (url: string) => void;
50
+
49
51
  const mockLocation = (mockedLocation: string) => {
50
- // We can't simply do 'window.location.href = defaultLocation;', as that
51
- // causes our test environment to try to navigate to the new location (as
52
- // a browser would do). So instead we need to reset 'window.location' as a
53
- // whole, and reset it's value after our test. We also need to
54
- // replace the 'history' object, otherwise this error happens: "SecurityError:
55
- // replaceState cannot update history to a URL which differs in components other
56
- // than in path, query, or fragment.""
57
-
58
- // (window as any) is used to override the window type definition and
59
- // allow location to be written.
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
- delete (window as any).location;
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- delete (window as any).history.replaceState;
64
-
65
- // Set our window's location to our test value.
66
- (window as any).location = {
67
- href: mockedLocation,
68
- } as Location;
69
- window.history.replaceState = jest.fn();
52
+ __setTestUrl(mockedLocation);
53
+ jest.spyOn(window.history, "replaceState").mockImplementation(() => {});
70
54
  };
71
55
 
72
56
  describe("Session", () => {
@@ -119,11 +103,10 @@ describe("Session", () => {
119
103
  // Let's make TypeScript and eslint angry! We'll set our window mock to
120
104
  // undefined so that any references to its properties or methods explode.
121
105
  // @ts-ignore-start
122
- // eslint-disable-next-line no-global-assign
106
+
123
107
  window = undefined;
124
108
  // @ts-ignore-end
125
109
  expect(() => {
126
- // eslint-disable-next-line no-new
127
110
  new Session({});
128
111
  }).not.toThrow();
129
112
  });
@@ -136,7 +119,7 @@ describe("Session", () => {
136
119
  const mySession = new Session({ clientAuthentication });
137
120
  // login never resolves if there are no errors,
138
121
  // because a login redirects the user away from the page:
139
- // eslint-disable-next-line no-void
122
+
140
123
  void mySession.login({});
141
124
  expect(clientAuthnLogin).toHaveBeenCalled();
142
125
  });
@@ -147,7 +130,7 @@ describe("Session", () => {
147
130
  const mySession = new Session({ clientAuthentication });
148
131
  // login never resolves if there are no errors,
149
132
  // because a login redirects the user away from the page:
150
- // eslint-disable-next-line no-void
133
+
151
134
  void mySession.login({
152
135
  tokenType: "Bearer",
153
136
  });
@@ -168,7 +151,7 @@ describe("Session", () => {
168
151
  };
169
152
  // login never resolves if there are no errors,
170
153
  // because a login redirects the user away from the page:
171
- // eslint-disable-next-line no-void
154
+
172
155
  void objectWithLogin.login({});
173
156
  expect(clientAuthnLogin).toHaveBeenCalled();
174
157
  });
@@ -235,7 +218,7 @@ describe("Session", () => {
235
218
 
236
219
  describe("handleIncomingRedirect", () => {
237
220
  it("uses current window location as default redirect URL", async () => {
238
- mockLocation("https://some.url");
221
+ mockLocation("https://some.url/");
239
222
  const clientAuthentication = mockClientAuthentication();
240
223
  const incomingRedirectHandler = jest
241
224
  .spyOn(clientAuthentication, "handleIncomingRedirect")
@@ -244,13 +227,13 @@ describe("Session", () => {
244
227
  const mySession = new Session({ clientAuthentication });
245
228
  await mySession.handleIncomingRedirect();
246
229
  expect(incomingRedirectHandler).toHaveBeenCalledWith(
247
- "https://some.url",
230
+ "https://some.url/",
248
231
  mySession.events,
249
232
  );
250
233
  });
251
234
 
252
235
  it("wraps ClientAuthentication handleIncomingRedirect", async () => {
253
- mockLocation("https://some.url");
236
+ mockLocation("https://some.url/");
254
237
  const clientAuthentication = mockClientAuthentication();
255
238
  const incomingRedirectHandler = jest
256
239
  .spyOn(clientAuthentication, "handleIncomingRedirect")
@@ -380,6 +363,7 @@ describe("Session", () => {
380
363
  "https://my.app/?code=someCode&state=arizona",
381
364
  );
382
365
  // We know that it has been set by the call to `blockingRequest`.
366
+ // FIXME: This can be fixed when we drop support for Node 20 by using Promise.withResolvers.
383
367
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
384
368
  continueAfterSecondRequest!();
385
369
  const tokenRequests = await Promise.all([
@@ -425,7 +409,7 @@ describe("Session", () => {
425
409
  incomingRedirectPromise,
426
410
  ) as typeof clientAuthentication.handleIncomingRedirect;
427
411
  const mySession = new Session({ clientAuthentication });
428
- // eslint-disable-next-line no-void
412
+
429
413
  void mySession.handleIncomingRedirect({
430
414
  url: "https://some.redirect/url",
431
415
  restorePreviousSession: true,
@@ -458,9 +442,12 @@ describe("Session", () => {
458
442
  .mockReturnValueOnce(
459
443
  incomingRedirectPromise,
460
444
  ) as typeof clientAuthentication.handleIncomingRedirect;
461
- const validateCurrentSessionPromise = Promise.resolve(
462
- "https://some.issuer/",
463
- );
445
+ const validateCurrentSessionPromise = Promise.resolve({
446
+ issuer: "https://some.issuer/",
447
+ clientAppId: "some client ID",
448
+ redirectUrl: "https://some.redirect/url",
449
+ tokenType: "DPoP",
450
+ });
464
451
  clientAuthentication.validateCurrentSession = jest
465
452
  .fn()
466
453
  .mockReturnValue(
@@ -468,7 +455,7 @@ describe("Session", () => {
468
455
  ) as typeof clientAuthentication.validateCurrentSession;
469
456
 
470
457
  const mySession = new Session({ clientAuthentication });
471
- // eslint-disable-next-line no-void
458
+
472
459
  void mySession.handleIncomingRedirect({
473
460
  url: "https://some.redirect/url",
474
461
  restorePreviousSession: true,
@@ -507,7 +494,7 @@ describe("Session", () => {
507
494
  undefined,
508
495
  ) as typeof clientAuthentication.handleIncomingRedirect;
509
496
  const mySession = new Session({ clientAuthentication });
510
- // eslint-disable-next-line no-void
497
+
511
498
  void mySession.handleIncomingRedirect({
512
499
  url: "https://some.redirect/url",
513
500
  restorePreviousSession: true,
@@ -536,6 +523,7 @@ describe("Session", () => {
536
523
  issuer: "https://some.issuer",
537
524
  clientAppId: "some client ID",
538
525
  clientAppSecret: "some client secret",
526
+ clientExpiresAt: Math.floor(Date.now() / 1000) + 10000,
539
527
  redirectUrl: "https://some.redirect/url",
540
528
  tokenType: "DPoP",
541
529
  });
@@ -553,7 +541,7 @@ describe("Session", () => {
553
541
  clientAuthentication.login = jest.fn<typeof clientAuthentication.login>();
554
542
 
555
543
  const mySession = new Session({ clientAuthentication });
556
- // eslint-disable-next-line no-void
544
+
557
545
  void mySession.handleIncomingRedirect({
558
546
  url: "https://some.redirect/url",
559
547
  restorePreviousSession: true,
@@ -652,7 +640,7 @@ describe("Session", () => {
652
640
  clientAuthentication.login = jest.fn<typeof clientAuthentication.login>();
653
641
 
654
642
  const mySession = new Session({ clientAuthentication });
655
- // eslint-disable-next-line no-void
643
+
656
644
  void mySession.handleIncomingRedirect("https://some.redirect/url");
657
645
  expect(window.localStorage.getItem(KEY_CURRENT_URL)).toBeNull();
658
646
  expect(clientAuthentication.login).not.toHaveBeenCalled();
@@ -689,7 +677,7 @@ describe("Session", () => {
689
677
  clientAuthentication.login = jest.fn<typeof clientAuthentication.login>();
690
678
 
691
679
  const mySession = new Session({ clientAuthentication });
692
- // eslint-disable-next-line no-void
680
+
693
681
  void mySession.handleIncomingRedirect({
694
682
  url: "https://some.redirect/url",
695
683
  restorePreviousSession: false,
@@ -728,7 +716,7 @@ describe("Session", () => {
728
716
  clientAuthentication.login = jest.fn<typeof clientAuthentication.login>();
729
717
 
730
718
  const mySession = new Session({ clientAuthentication });
731
- // eslint-disable-next-line no-void
719
+
732
720
  void mySession.handleIncomingRedirect("https://some.redirect/url");
733
721
  expect(mySession.info.isLoggedIn).toBe(false);
734
722
  // The local storage should have been cleared by the auth error
@@ -761,6 +749,53 @@ describe("Session", () => {
761
749
  // The local storage should have been cleared by the auth error
762
750
  expect(window.localStorage.getItem(KEY_CURRENT_SESSION)).toBeNull();
763
751
  });
752
+
753
+ it("does not attempt silent authentication if the stored client has expired", async () => {
754
+ const sessionId = "mySession";
755
+ mockLocalStorage({
756
+ [KEY_CURRENT_SESSION]: sessionId,
757
+ });
758
+ mockLocation("https://mock.current/location");
759
+
760
+ // Set up storage with an expired client (expiresAt in the past)
761
+ const expiredAt = Math.floor(Date.now() / 1000) - 1000;
762
+ const mockedStorage = new StorageUtility(
763
+ mockStorage({
764
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
765
+ isLoggedIn: "true",
766
+ webId: "https://my.pod/profile#me",
767
+ },
768
+ }),
769
+ mockStorage({
770
+ [`${USER_SESSION_PREFIX}:${sessionId}`]: {
771
+ clientId: "https://some.app/registration",
772
+ clientSecret: "some-secret",
773
+ issuer: "https://some.issuer",
774
+ expiresAt: String(expiredAt),
775
+ },
776
+ }),
777
+ );
778
+ const clientAuthentication = mockClientAuthentication({
779
+ sessionInfoManager: mockSessionInfoManager(mockedStorage),
780
+ });
781
+
782
+ // Mock handleIncomingRedirect to return undefined (no OAuth params in URL)
783
+ clientAuthentication.handleIncomingRedirect = jest
784
+ .fn<typeof clientAuthentication.handleIncomingRedirect>()
785
+ .mockResolvedValue(undefined);
786
+ clientAuthentication.login = jest.fn<typeof clientAuthentication.login>();
787
+
788
+ const mySession = new Session({ clientAuthentication });
789
+ const result = await mySession.handleIncomingRedirect({
790
+ url: "https://some.redirect/url",
791
+ restorePreviousSession: true,
792
+ });
793
+
794
+ // Silent auth should NOT have been attempted because client is expired
795
+ expect(clientAuthentication.login).not.toHaveBeenCalled();
796
+ // The function should resolve (not hang)
797
+ expect(result).toBeUndefined();
798
+ });
764
799
  });
765
800
 
766
801
  describe("events.on", () => {
package/src/Session.ts CHANGED
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -31,6 +30,7 @@ import {
31
30
  mockStorageUtility,
32
31
  mockIncomingRedirectHandler,
33
32
  mockLogoutHandler,
33
+ // eslint-disable-next-line import/no-unresolved
34
34
  } from "@inrupt/solid-client-authn-core/mocks";
35
35
 
36
36
  import ClientAuthentication from "../ClientAuthentication";
package/src/constant.ts CHANGED
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -48,7 +47,7 @@ export function getDefaultSession(): Session {
48
47
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch}
49
48
  * @since 1.3.0
50
49
  */
51
- /* eslint-disable-next-line no-shadow */
50
+
52
51
  export function fetch(
53
52
  ...args: Parameters<Session["fetch"]>
54
53
  ): ReturnType<Session["fetch"]> {
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -1,4 +1,3 @@
1
- //
2
1
  // Copyright Inrupt Inc.
3
2
  //
4
3
  // Permission is hereby granted, free of charge, to any person obtaining a copy