@inrupt/solid-client-authn-browser 1.13.4 → 1.15.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 (38) hide show
  1. package/dist/ClientAuthentication.d.ts +1 -1
  2. package/dist/ClientAuthentication.js +2 -1
  3. package/dist/ClientAuthentication.js.map +1 -1
  4. package/dist/Session.d.ts +5 -3
  5. package/dist/Session.js +21 -17
  6. package/dist/Session.js.map +1 -1
  7. package/dist/defaultSession.d.ts +1 -0
  8. package/dist/defaultSession.js +5 -1
  9. package/dist/defaultSession.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +2 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/login/oidc/IssuerConfigFetcher.js +2 -1
  14. package/dist/login/oidc/IssuerConfigFetcher.js.map +1 -1
  15. package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.d.ts +1 -0
  16. package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.js +2 -1
  17. package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.js.map +1 -1
  18. package/dist/sessionInfo/SessionInfoManager.d.ts +1 -0
  19. package/dist/sessionInfo/SessionInfoManager.js +2 -1
  20. package/dist/sessionInfo/SessionInfoManager.js.map +1 -1
  21. package/dist/solid-client-authn.bundle.js +1 -1
  22. package/package.json +5 -7
  23. package/src/ClientAuthentication.spec.ts +15 -8
  24. package/src/ClientAuthentication.ts +2 -2
  25. package/src/Session.spec.ts +255 -119
  26. package/src/Session.ts +44 -18
  27. package/src/defaultSession.spec.ts +28 -19
  28. package/src/defaultSession.ts +15 -1
  29. package/src/index.ts +1 -0
  30. package/src/login/oidc/ClientRegistrar.spec.ts +1 -1
  31. package/src/login/oidc/IssuerConfigFetcher.spec.ts +60 -40
  32. package/src/login/oidc/IssuerConfigFetcher.ts +2 -1
  33. package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.spec.ts +27 -22
  34. package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +2 -0
  35. package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.spec.ts +12 -3
  36. package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.spec.ts +12 -1
  37. package/src/sessionInfo/SessionInfoManager.ts +1 -0
  38. package/tsconfig.json +2 -1
package/src/Session.ts CHANGED
@@ -22,14 +22,18 @@
22
22
  /**
23
23
  * @hidden
24
24
  */
25
- import { EventEmitter } from "events";
26
25
  import {
27
26
  EVENTS,
28
27
  ILoginInputOptions,
29
28
  ISessionInfo,
30
29
  IStorage,
30
+ IHasSessionEventListener,
31
+ ISessionEventListener,
32
+ buildProxyHandler,
31
33
  } from "@inrupt/solid-client-authn-core";
34
+ import { fetch } from "@inrupt/universal-fetch";
32
35
  import { v4 } from "uuid";
36
+ import EventEmitter from "events";
33
37
  import ClientAuthentication from "./ClientAuthentication";
34
38
  import { getClientAuthenticationWithDependencies } from "./dependencies";
35
39
  import { KEY_CURRENT_SESSION, KEY_CURRENT_URL } from "./constant";
@@ -116,7 +120,7 @@ export async function silentlyAuthenticate(
116
120
  clientSecret: storedSessionInfo.clientAppSecret,
117
121
  tokenType: storedSessionInfo.tokenType ?? "DPoP",
118
122
  },
119
- session
123
+ session.events
120
124
  );
121
125
  return true;
122
126
  }
@@ -132,12 +136,19 @@ function isLoggedIn(
132
136
  /**
133
137
  * A {@link Session} object represents a user's session on an application. The session holds state, as it stores information enabling acces to private resources after login for instance.
134
138
  */
135
- export class Session extends EventEmitter {
139
+ export class Session extends EventEmitter implements IHasSessionEventListener {
136
140
  /**
137
141
  * Information regarding the current session.
138
142
  */
139
143
  public readonly info: ISessionInfo;
140
144
 
145
+ /**
146
+ * Session attribute exposing the EventEmitter interface, to listen on session
147
+ * events such as login, logout, etc.
148
+ * @since 1.15.0
149
+ */
150
+ public readonly events: ISessionEventListener;
151
+
141
152
  private clientAuthentication: ClientAuthentication;
142
153
 
143
154
  private tokenRequestInProgress = false;
@@ -162,7 +173,17 @@ export class Session extends EventEmitter {
162
173
  sessionId: string | undefined = undefined
163
174
  ) {
164
175
  super();
165
-
176
+ // Until Session no longer implements EventEmitter, this.events is just a proxy
177
+ // to this (with some interface filtering). When we make the breaking change,
178
+ // this.events will be a regular EventEmitter (implementing ISessionEventEmitter):
179
+ // this.events = new EventEmitter();
180
+ this.events = new Proxy(
181
+ this,
182
+ buildProxyHandler(
183
+ Session.prototype,
184
+ "events only implements ISessionEventListener"
185
+ )
186
+ );
166
187
  if (sessionOptions.clientAuthentication) {
167
188
  this.clientAuthentication = sessionOptions.clientAuthentication;
168
189
  } else if (sessionOptions.secureStorage && sessionOptions.insecureStorage) {
@@ -191,13 +212,13 @@ export class Session extends EventEmitter {
191
212
  // enable silent refresh. The current session ID specifically stored in 'localStorage'
192
213
  // (as opposed to using our storage abstraction layer) because it is only
193
214
  // used in a browser-specific mechanism.
194
- this.on(EVENTS.LOGIN, () =>
215
+ this.events.on(EVENTS.LOGIN, () =>
195
216
  window.localStorage.setItem(KEY_CURRENT_SESSION, this.info.sessionId)
196
217
  );
197
218
 
198
- this.on(EVENTS.SESSION_EXPIRED, () => this.internalLogout(false));
219
+ this.events.on(EVENTS.SESSION_EXPIRED, () => this.internalLogout(false));
199
220
 
200
- this.on(EVENTS.ERROR, () => this.internalLogout(false));
221
+ this.events.on(EVENTS.ERROR, () => this.internalLogout(false));
201
222
  }
202
223
 
203
224
  /**
@@ -216,7 +237,7 @@ export class Session extends EventEmitter {
216
237
  // Defaults the token type to DPoP
217
238
  tokenType: options.tokenType ?? "DPoP",
218
239
  },
219
- this
240
+ this.events
220
241
  );
221
242
  // `login` redirects the user away from the app,
222
243
  // so unless it throws an error, there is no code that should run afterwards
@@ -250,7 +271,7 @@ export class Session extends EventEmitter {
250
271
  await this.clientAuthentication.logout(this.info.sessionId);
251
272
  this.info.isLoggedIn = false;
252
273
  if (emitSignal) {
253
- this.emit(EVENTS.LOGOUT);
274
+ (this.events as EventEmitter).emit(EVENTS.LOGOUT);
254
275
  }
255
276
  };
256
277
 
@@ -282,7 +303,7 @@ export class Session extends EventEmitter {
282
303
  this.tokenRequestInProgress = true;
283
304
  const sessionInfo = await this.clientAuthentication.handleIncomingRedirect(
284
305
  url,
285
- this
306
+ this.events
286
307
  );
287
308
  if (isLoggedIn(sessionInfo)) {
288
309
  this.setSessionInfo(sessionInfo);
@@ -290,13 +311,13 @@ export class Session extends EventEmitter {
290
311
  if (currentUrl === null) {
291
312
  // The login event can only be triggered **after** the user has been
292
313
  // redirected from the IdP with access and ID tokens.
293
- this.emit(EVENTS.LOGIN);
314
+ (this.events as EventEmitter).emit(EVENTS.LOGIN);
294
315
  } else {
295
316
  // If an URL is stored in local storage, we are being logged in after a
296
317
  // silent authentication, so remove our currently stored URL location
297
318
  // to clean up our state now that we are completing the re-login process.
298
319
  window.localStorage.removeItem(KEY_CURRENT_URL);
299
- this.emit(EVENTS.SESSION_RESTORED, currentUrl);
320
+ (this.events as EventEmitter).emit(EVENTS.SESSION_RESTORED, currentUrl);
300
321
  }
301
322
  } else if (options.restorePreviousSession === true) {
302
323
  // Silent authentication happens after a refresh, which means there are no
@@ -332,18 +353,20 @@ export class Session extends EventEmitter {
332
353
  * The callback is called when {@link handleIncomingRedirect} completes successfully.
333
354
  *
334
355
  * @param callback The function called when a user completes login.
356
+ * @deprecated Prefer session.events.on(EVENTS.LOGIN, callback)
335
357
  */
336
358
  onLogin(callback: () => unknown): void {
337
- this.on(EVENTS.LOGIN, callback);
359
+ this.events.on(EVENTS.LOGIN, callback);
338
360
  }
339
361
 
340
362
  /**
341
363
  * Register a callback function to be called when a user logs out:
342
364
  *
343
365
  * @param callback The function called when a user completes logout.
366
+ * @deprecated Prefer session.events.on(EVENTS.LOGOUT, callback)
344
367
  */
345
368
  onLogout(callback: () => unknown): void {
346
- this.on(EVENTS.LOGOUT, callback);
369
+ this.events.on(EVENTS.LOGOUT, callback);
347
370
  }
348
371
 
349
372
  /**
@@ -351,6 +374,7 @@ export class Session extends EventEmitter {
351
374
  *
352
375
  * @param callback The function called when an error occurs.
353
376
  * @since 1.11.0
377
+ * @deprecated Prefer session.events.on(EVENTS.ERROR, callback)
354
378
  */
355
379
  onError(
356
380
  callback: (
@@ -358,7 +382,7 @@ export class Session extends EventEmitter {
358
382
  errorDescription?: string | null
359
383
  ) => unknown
360
384
  ): void {
361
- this.on(EVENTS.ERROR, callback);
385
+ this.events.on(EVENTS.ERROR, callback);
362
386
  }
363
387
 
364
388
  /**
@@ -368,9 +392,10 @@ export class Session extends EventEmitter {
368
392
  * at the time the session was restored.
369
393
  *
370
394
  * @param callback The function called when a user's already logged-in session is restored, e.g., after a silent authentication is completed after a page refresh.
395
+ * @deprecated Prefer session.events.on(EVENTS.SESSION_RESTORED, callback)
371
396
  */
372
397
  onSessionRestore(callback: (currentUrl: string) => unknown): void {
373
- this.on(EVENTS.SESSION_RESTORED, callback);
398
+ this.events.on(EVENTS.SESSION_RESTORED, callback);
374
399
  }
375
400
 
376
401
  /**
@@ -378,9 +403,10 @@ export class Session extends EventEmitter {
378
403
  * make authenticated requests, but following a user logout.
379
404
  * @param callback The function that runs on session expiration.
380
405
  * @since 1.11.0
406
+ * @deprecated Prefer session.events.on(EVENTS.SESSION_EXPIRED, callback)
381
407
  */
382
408
  onSessionExpiration(callback: () => unknown): void {
383
- this.on(EVENTS.SESSION_EXPIRED, callback);
409
+ this.events.on(EVENTS.SESSION_EXPIRED, callback);
384
410
  }
385
411
 
386
412
  private setSessionInfo(
@@ -390,7 +416,7 @@ export class Session extends EventEmitter {
390
416
  this.info.webId = sessionInfo.webId;
391
417
  this.info.sessionId = sessionInfo.sessionId;
392
418
  this.info.expirationDate = sessionInfo.expirationDate;
393
- this.on(EVENTS.SESSION_EXTENDED, (expiresIn: number) => {
419
+ this.events.on(EVENTS.SESSION_EXTENDED, (expiresIn: number) => {
394
420
  this.info.expirationDate = Date.now() + expiresIn * 1000;
395
421
  });
396
422
  }
@@ -19,6 +19,7 @@
19
19
  // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
20
  //
21
21
 
22
+ import { EVENTS } from "@inrupt/solid-client-authn-core";
22
23
  import { jest, it, expect } from "@jest/globals";
23
24
  import {
24
25
  fetch,
@@ -29,33 +30,34 @@ import {
29
30
  onLogin,
30
31
  onSessionRestore,
31
32
  onLogout,
33
+ events,
32
34
  } from "./defaultSession";
33
- import type * as SessionModuleType from "./Session";
34
-
35
- jest.mock("../src/Session.ts");
36
-
37
- it("does not instantiate a Session without calling one of its methods", () => {
38
- const mockedSession = jest.requireMock(
39
- "../src/Session.ts"
40
- ) as typeof SessionModuleType;
41
-
42
- expect(mockedSession.Session).not.toHaveBeenCalled();
35
+ import * as SessionModule from "./Session";
36
+
37
+ it("instantiates the default session lazily", () => {
38
+ const singletonSession = new SessionModule.Session();
39
+ const spiedConstructor = jest
40
+ .spyOn(SessionModule, "Session")
41
+ .mockImplementation(() => singletonSession);
42
+ expect(spiedConstructor).not.toHaveBeenCalled();
43
+ events().on(EVENTS.LOGIN, jest.fn());
44
+ expect(spiedConstructor).toHaveBeenCalled();
43
45
  });
44
46
 
45
47
  it("re-uses the same Session when calling multiple methods", () => {
46
- const mockedSession = jest.requireMock(
47
- "../src/Session.ts"
48
- ) as typeof SessionModuleType;
48
+ const singletonSession = new SessionModule.Session();
49
+ const spiedConstructor = jest
50
+ .spyOn(SessionModule, "Session")
51
+ .mockImplementation(() => singletonSession);
49
52
 
50
- expect(mockedSession.Session).not.toHaveBeenCalled();
53
+ events().on(EVENTS.LOGIN, jest.fn());
51
54
 
52
- onLogin(jest.fn());
55
+ expect(spiedConstructor).toHaveBeenCalledTimes(1);
53
56
 
54
- expect(mockedSession.Session).toHaveBeenCalledTimes(1);
57
+ events().on(EVENTS.LOGOUT, jest.fn());
55
58
 
56
- onLogout(jest.fn());
57
-
58
- expect(mockedSession.Session).toHaveBeenCalledTimes(1);
59
+ // No new session has been instantiated.
60
+ expect(spiedConstructor).toHaveBeenCalledTimes(1);
59
61
  });
60
62
 
61
63
  it("all functions pass on their arguments to the default session", () => {
@@ -73,6 +75,7 @@ it("all functions pass on their arguments to the default session", () => {
73
75
  const onLoginSpy = jest.spyOn(defaultSession, "onLogin");
74
76
  const onLogoutSpy = jest.spyOn(defaultSession, "onLogout");
75
77
  const onSessionRestoreSpy = jest.spyOn(defaultSession, "onSessionRestore");
78
+ const eventsOnSpy = jest.spyOn(defaultSession.events, "on");
76
79
 
77
80
  expect(fetchSpy).not.toHaveBeenCalled();
78
81
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -94,6 +97,12 @@ it("all functions pass on their arguments to the default session", () => {
94
97
  handleIncomingRedirect("https://example.com");
95
98
  expect(handleIncomingRedirectSpy).toHaveBeenCalledTimes(1);
96
99
 
100
+ // onLogin will internall call events.on(...), so the following test
101
+ // must come first.
102
+ expect(eventsOnSpy).not.toHaveBeenCalled();
103
+ events().on(EVENTS.LOGIN, jest.fn());
104
+ expect(eventsOnSpy).toHaveBeenCalledTimes(1);
105
+
97
106
  expect(onLoginSpy).not.toHaveBeenCalled();
98
107
  onLogin(jest.fn());
99
108
  expect(onLoginSpy).toHaveBeenCalledTimes(1);
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { Session } from "./Session";
23
23
 
24
- let defaultSession: Session;
24
+ let defaultSession: Session | undefined;
25
25
 
26
26
  /**
27
27
  * Obtain the {@link Session} used when not explicitly instantiating one yourself.
@@ -97,6 +97,8 @@ export const handleIncomingRedirect: Session["handleIncomingRedirect"] = (
97
97
  * @since 1.3.0
98
98
  *
99
99
  * @param callback The function called when a user completes login.
100
+ * @deprecated Prefer events.on(EVENTS.LOGIN, callback)
101
+
100
102
  */
101
103
  export const onLogin: Session["onLogin"] = (...args) => {
102
104
  const session = getDefaultSession();
@@ -108,6 +110,8 @@ export const onLogin: Session["onLogin"] = (...args) => {
108
110
  *
109
111
  * @param callback The function called when a user completes logout.
110
112
  * @since 1.3.0
113
+ * @deprecated Prefer events.on(EVENTS.LOGOUT, callback)
114
+
111
115
  */
112
116
  export const onLogout: Session["onLogout"] = (...args) => {
113
117
  const session = getDefaultSession();
@@ -119,8 +123,18 @@ export const onLogout: Session["onLogout"] = (...args) => {
119
123
  *
120
124
  * @param callback The function called when a session is restored.
121
125
  * @since 1.3.0
126
+ * @deprecated Prefer events.on(EVENTS.SESSION_RESTORED, callback)
122
127
  */
123
128
  export const onSessionRestore: Session["onSessionRestore"] = (...args) => {
124
129
  const session = getDefaultSession();
125
130
  return session.onSessionRestore(...args);
126
131
  };
132
+
133
+ /**
134
+ * {@link SessionEventEmitter} instance to subscribe to events by the default session.
135
+ *
136
+ * @since Unreleased
137
+ */
138
+ export const events = (): Session["events"] => {
139
+ return getDefaultSession().events;
140
+ };
package/src/index.ts CHANGED
@@ -34,4 +34,5 @@ export {
34
34
  NotImplementedError,
35
35
  ConfigurationError,
36
36
  InMemoryStorage,
37
+ EVENTS,
37
38
  } from "@inrupt/solid-client-authn-core";
@@ -24,7 +24,7 @@ import {
24
24
  mockStorageUtility,
25
25
  } from "@inrupt/solid-client-authn-core/mocks";
26
26
  import { jest, it, describe, expect } from "@jest/globals";
27
- import { Response as NodeResponse } from "cross-fetch";
27
+ import { Response as NodeResponse } from "@inrupt/universal-fetch";
28
28
  import ClientRegistrar from "./ClientRegistrar";
29
29
  import { IssuerConfigFetcherFetchConfigResponse } from "./__mocks__/IssuerConfigFetcher";
30
30
 
@@ -21,9 +21,20 @@
21
21
 
22
22
  import { jest, it, describe, expect } from "@jest/globals";
23
23
  import { mockStorageUtility } from "@inrupt/solid-client-authn-core";
24
- import { Response as NodeResponse } from "cross-fetch";
24
+ import { fetch, Response as NodeResponse } from "@inrupt/universal-fetch";
25
+ import * as UniversalFetch from "@inrupt/universal-fetch";
25
26
  import IssuerConfigFetcher from "./IssuerConfigFetcher";
26
27
 
28
+ jest.mock("@inrupt/universal-fetch", () => {
29
+ const fetchModule = jest.requireActual(
30
+ "@inrupt/universal-fetch"
31
+ ) as typeof UniversalFetch;
32
+ return {
33
+ ...fetchModule,
34
+ fetch: jest.fn<typeof fetch>(),
35
+ };
36
+ });
37
+
27
38
  /**
28
39
  * Test for IssuerConfigFetcher
29
40
  */
@@ -41,19 +52,22 @@ describe("IssuerConfigFetcher", () => {
41
52
  }
42
53
 
43
54
  it("should return a config based on the fetched config if none was stored in the storage", async () => {
44
- const fetchResponse = new NodeResponse(
45
- JSON.stringify({
46
- issuer: "https://example.com",
47
- // eslint-disable-next-line camelcase
48
- claim_types_supported: "oidc",
49
- bleepBloop: "Meep Moop",
50
- })
51
- ) as unknown as Response;
52
55
  const configFetcher = getIssuerConfigFetcher({
53
56
  storageUtility: mockStorageUtility({}),
54
57
  });
55
- const mockFetch = (jest.fn() as any).mockResolvedValueOnce(fetchResponse);
56
- window.fetch = mockFetch as typeof window.fetch;
58
+ const { fetch: mockFetch } = jest.requireMock(
59
+ "@inrupt/universal-fetch"
60
+ ) as jest.Mocked<typeof UniversalFetch>;
61
+ mockFetch.mockResolvedValueOnce(
62
+ new NodeResponse(
63
+ JSON.stringify({
64
+ issuer: "https://example.com",
65
+ // eslint-disable-next-line camelcase
66
+ claim_types_supported: "oidc",
67
+ bleepBloop: "Meep Moop",
68
+ })
69
+ )
70
+ );
57
71
  const fetchedConfig = await configFetcher.fetchConfig(
58
72
  "https://arbitrary.url"
59
73
  );
@@ -65,34 +79,34 @@ describe("IssuerConfigFetcher", () => {
65
79
  });
66
80
 
67
81
  it("should throw an error if the fetched config could not be converted to JSON", async () => {
68
- const mockFetch = (): Promise<Response> =>
69
- Promise.resolve({
70
- json: () => {
71
- throw new Error("Some error");
72
- },
73
- } as unknown as Response);
74
- window.fetch = mockFetch;
82
+ const { fetch: mockFetch } = jest.requireMock(
83
+ "@inrupt/universal-fetch"
84
+ ) as jest.Mocked<typeof UniversalFetch>;
85
+ mockFetch.mockResolvedValueOnce(new NodeResponse("Not JSON."));
75
86
  const configFetcher = new IssuerConfigFetcher(mockStorageUtility({}));
76
87
 
77
88
  await expect(configFetcher.fetchConfig("https://some.url")).rejects.toThrow(
78
- "[https://some.url] has an invalid configuration: Some error"
89
+ "[https://some.url] has an invalid configuration:"
79
90
  );
80
91
  });
81
92
 
82
93
  it("should return a config including the support for solid-oidc if present in the discovery profile", async () => {
83
- const fetchResponse = new NodeResponse(
84
- JSON.stringify({
85
- issuer: "https://example.com",
86
- // eslint-disable-next-line camelcase
87
- claim_types_supported: "oidc",
88
- scopes_supported: ["openid", "offline_access", "webid"],
89
- })
90
- ) as unknown as Response;
94
+ const { fetch: mockFetch } = jest.requireMock(
95
+ "@inrupt/universal-fetch"
96
+ ) as jest.Mocked<typeof UniversalFetch>;
97
+ mockFetch.mockResolvedValueOnce(
98
+ new NodeResponse(
99
+ JSON.stringify({
100
+ issuer: "https://example.com",
101
+ // eslint-disable-next-line camelcase
102
+ claim_types_supported: "oidc",
103
+ scopes_supported: ["openid", "offline_access", "webid"],
104
+ })
105
+ )
106
+ );
91
107
  const configFetcher = getIssuerConfigFetcher({
92
108
  storageUtility: mockStorageUtility({}),
93
109
  });
94
- const mockFetch = (jest.fn() as any).mockResolvedValueOnce(fetchResponse);
95
- window.fetch = mockFetch as typeof window.fetch;
96
110
  const fetchedConfig = await configFetcher.fetchConfig(
97
111
  "https://arbitrary.url"
98
112
  );
@@ -102,18 +116,21 @@ describe("IssuerConfigFetcher", () => {
102
116
  });
103
117
 
104
118
  it("should return a default value for the supported scopes if not advertized by the OpenID provider", async () => {
105
- const fetchResponse = new NodeResponse(
106
- JSON.stringify({
107
- issuer: "https://example.com",
108
- // eslint-disable-next-line camelcase
109
- claim_types_supported: "oidc",
110
- })
111
- ) as unknown as Response;
119
+ const { fetch: mockFetch } = jest.requireMock(
120
+ "@inrupt/universal-fetch"
121
+ ) as jest.Mocked<typeof UniversalFetch>;
122
+ mockFetch.mockResolvedValueOnce(
123
+ new NodeResponse(
124
+ JSON.stringify({
125
+ issuer: "https://example.com",
126
+ // eslint-disable-next-line camelcase
127
+ claim_types_supported: "oidc",
128
+ })
129
+ )
130
+ );
112
131
  const configFetcher = getIssuerConfigFetcher({
113
132
  storageUtility: mockStorageUtility({}),
114
133
  });
115
- const mockFetch = (jest.fn() as any).mockResolvedValueOnce(fetchResponse);
116
- window.fetch = mockFetch as typeof window.fetch;
117
134
  const fetchedConfig = await configFetcher.fetchConfig(
118
135
  "https://arbitrary.url"
119
136
  );
@@ -122,17 +139,20 @@ describe("IssuerConfigFetcher", () => {
122
139
 
123
140
  it("should append the .well-known/openid-configuration path at the end of the issuer URL", async () => {
124
141
  // The response value is irrelevant to this test.
125
- const mockFetch = jest.fn<typeof fetch>().mockImplementation(
142
+ const { fetch: mockFetch } = jest.requireMock(
143
+ "@inrupt/universal-fetch"
144
+ ) as jest.Mocked<typeof UniversalFetch>;
145
+ mockFetch.mockImplementation(
126
146
  async () =>
127
147
  new NodeResponse(
128
148
  JSON.stringify({
129
149
  issuer: "https://example.com",
130
150
  // eslint-disable-next-line camelcase
131
151
  claim_types_supported: "oidc",
152
+ scopes_supported: ["openid", "offline_access", "webid"],
132
153
  })
133
154
  )
134
155
  );
135
- window.fetch = mockFetch as typeof window.fetch;
136
156
  const configFetcher = getIssuerConfigFetcher({
137
157
  storageUtility: mockStorageUtility({}),
138
158
  });
@@ -33,6 +33,7 @@ import {
33
33
  IStorageUtility,
34
34
  ConfigurationError,
35
35
  } from "@inrupt/solid-client-authn-core";
36
+ import { fetch } from "@inrupt/universal-fetch";
36
37
 
37
38
  export const WELL_KNOWN_OPENID_CONFIG = ".well-known/openid-configuration";
38
39
 
@@ -165,7 +166,7 @@ export default class IssuerConfigFetcher implements IIssuerConfigFetcher {
165
166
  // includes the full issuer path. See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig.
166
167
  issuer.endsWith("/") ? issuer : `${issuer}/`
167
168
  ).href;
168
- const issuerConfigRequestBody = await window.fetch(openIdConfigUrl);
169
+ const issuerConfigRequestBody = await fetch(openIdConfigUrl);
169
170
  // Check the validity of the fetched config
170
171
  try {
171
172
  issuerConfig = processConfig(await issuerConfigRequestBody.json());
@@ -35,7 +35,8 @@ import type * as SolidClientAuthnCore from "@inrupt/solid-client-authn-core";
35
35
  import { jest, it, describe, expect } from "@jest/globals";
36
36
  import { CodeExchangeResult, getBearerToken } from "@inrupt/oidc-client-ext";
37
37
  import type * as OidcClientExt from "@inrupt/oidc-client-ext";
38
- import { Response, Headers } from "cross-fetch";
38
+ import { fetch, Response, Headers } from "@inrupt/universal-fetch";
39
+ import * as UniversalFetch from "@inrupt/universal-fetch";
39
40
  import { JWK, importJWK } from "jose";
40
41
  import { KeyObject } from "crypto";
41
42
  import { AuthCodeRedirectHandler } from "./AuthCodeRedirectHandler";
@@ -46,6 +47,16 @@ import {
46
47
  mockTokenRefresher,
47
48
  } from "../refresh/__mocks__/TokenRefresher";
48
49
 
50
+ jest.mock("@inrupt/universal-fetch", () => {
51
+ const fetchModule = jest.requireActual(
52
+ "@inrupt/universal-fetch"
53
+ ) as typeof UniversalFetch;
54
+ return {
55
+ ...fetchModule,
56
+ fetch: jest.fn<typeof fetch>(),
57
+ };
58
+ });
59
+
49
60
  const mockJwk = (): JWK => {
50
61
  return {
51
62
  kty: "EC",
@@ -193,8 +204,10 @@ function mockClientRegistrar(client: IClient): IClientRegistrar {
193
204
  }
194
205
 
195
206
  const mockFetch = (response: Response) => {
196
- const mockedFetch = jest.fn(global.fetch).mockResolvedValue(response);
197
- window.fetch = mockedFetch;
207
+ const { fetch: mockedFetch } = jest.requireMock(
208
+ "@inrupt/universal-fetch"
209
+ ) as jest.Mocked<typeof UniversalFetch>;
210
+ mockedFetch.mockResolvedValueOnce(response);
198
211
  return mockedFetch;
199
212
  };
200
213
 
@@ -405,7 +418,7 @@ describe("AuthCodeRedirectHandler", () => {
405
418
 
406
419
  it("returns an authenticated bearer fetch if requested", async () => {
407
420
  mockOidcClient();
408
- const mockedFetch = jest.mocked<typeof fetch>(window.fetch);
421
+ const mockedFetch = jest.mocked<typeof fetch>(fetch);
409
422
  mockedFetch.mockResolvedValueOnce(
410
423
  new Response("", {
411
424
  status: 200,
@@ -433,7 +446,7 @@ describe("AuthCodeRedirectHandler", () => {
433
446
 
434
447
  it("returns an authenticated DPoP fetch if requested", async () => {
435
448
  mockOidcClient();
436
- const mockedFetch = jest.mocked<typeof fetch>(window.fetch);
449
+ const mockedFetch = jest.mocked<typeof fetch>(fetch);
437
450
  mockedFetch.mockResolvedValueOnce(
438
451
  new Response("", {
439
452
  status: 200,
@@ -454,13 +467,9 @@ describe("AuthCodeRedirectHandler", () => {
454
467
 
455
468
  it("saves session information in storage on successful login", async () => {
456
469
  mockOidcClient();
457
- window.fetch = jest.fn(global.fetch).mockReturnValue(
458
- new Promise((resolve) => {
459
- resolve(
460
- new Response("", {
461
- status: 200,
462
- })
463
- );
470
+ mockFetch(
471
+ new Response("", {
472
+ status: 200,
464
473
  })
465
474
  );
466
475
 
@@ -482,15 +491,11 @@ describe("AuthCodeRedirectHandler", () => {
482
491
  it("does not override the previously stored redirect URL", async () => {
483
492
  mockOidcClient();
484
493
 
485
- window.fetch = (jest.fn() as any).mockReturnValue(
486
- new Promise((resolve) => {
487
- resolve(
488
- new Response("", {
489
- status: 200,
490
- })
491
- );
494
+ mockFetch(
495
+ new Response("", {
496
+ status: 200,
492
497
  })
493
- ) as jest.Mock<typeof fetch>;
498
+ );
494
499
 
495
500
  const mockedStorage = mockDefaultStorageUtility();
496
501
 
@@ -510,7 +515,7 @@ describe("AuthCodeRedirectHandler", () => {
510
515
 
511
516
  it("returns the expiration time normalised to the current time", async () => {
512
517
  mockOidcClient();
513
- window.fetch = jest.fn<typeof fetch>();
518
+ mockFetch(new Response());
514
519
  const MOCK_TIMESTAMP = 10000;
515
520
  Date.now = jest
516
521
  .fn()
@@ -533,7 +538,7 @@ describe("AuthCodeRedirectHandler", () => {
533
538
 
534
539
  it("returns null for the expiration time if none was provided", async () => {
535
540
  const mockedOidcClient = mockOidcClient();
536
- window.fetch = jest.fn<typeof fetch>();
541
+ mockFetch(new Response());
537
542
  mockedOidcClient.getBearerToken = jest
538
543
  .fn(getBearerToken)
539
544
  .mockResolvedValueOnce({
@@ -27,6 +27,8 @@
27
27
  // being unauthenticated. This should be looked into when migrating to universal-fetch.
28
28
  // import { fetch } from "cross-fetch";
29
29
 
30
+ import { fetch } from "@inrupt/universal-fetch";
31
+
30
32
  import {
31
33
  buildAuthenticatedFetch,
32
34
  IClient,