@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.
- package/dist/ClientAuthentication.d.ts +1 -1
- package/dist/ClientAuthentication.js +2 -1
- package/dist/ClientAuthentication.js.map +1 -1
- package/dist/Session.d.ts +5 -3
- package/dist/Session.js +21 -17
- package/dist/Session.js.map +1 -1
- package/dist/defaultSession.d.ts +1 -0
- package/dist/defaultSession.js +5 -1
- package/dist/defaultSession.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/login/oidc/IssuerConfigFetcher.js +2 -1
- package/dist/login/oidc/IssuerConfigFetcher.js.map +1 -1
- package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.d.ts +1 -0
- package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.js +2 -1
- package/dist/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.js.map +1 -1
- package/dist/sessionInfo/SessionInfoManager.d.ts +1 -0
- package/dist/sessionInfo/SessionInfoManager.js +2 -1
- package/dist/sessionInfo/SessionInfoManager.js.map +1 -1
- package/dist/solid-client-authn.bundle.js +1 -1
- package/package.json +5 -7
- package/src/ClientAuthentication.spec.ts +15 -8
- package/src/ClientAuthentication.ts +2 -2
- package/src/Session.spec.ts +255 -119
- package/src/Session.ts +44 -18
- package/src/defaultSession.spec.ts +28 -19
- package/src/defaultSession.ts +15 -1
- package/src/index.ts +1 -0
- package/src/login/oidc/ClientRegistrar.spec.ts +1 -1
- package/src/login/oidc/IssuerConfigFetcher.spec.ts +60 -40
- package/src/login/oidc/IssuerConfigFetcher.ts +2 -1
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.spec.ts +27 -22
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +2 -0
- package/src/login/oidc/incomingRedirectHandler/ErrorOidcHandler.spec.ts +12 -3
- package/src/login/oidc/incomingRedirectHandler/FallbackRedirectHandler.spec.ts +12 -1
- package/src/sessionInfo/SessionInfoManager.ts +1 -0
- 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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
expect(
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const singletonSession = new SessionModule.Session();
|
|
49
|
+
const spiedConstructor = jest
|
|
50
|
+
.spyOn(SessionModule, "Session")
|
|
51
|
+
.mockImplementation(() => singletonSession);
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
events().on(EVENTS.LOGIN, jest.fn());
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
expect(spiedConstructor).toHaveBeenCalledTimes(1);
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
events().on(EVENTS.LOGOUT, jest.fn());
|
|
55
58
|
|
|
56
|
-
|
|
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);
|
package/src/defaultSession.ts
CHANGED
|
@@ -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
|
@@ -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 "
|
|
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 "
|
|
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 =
|
|
56
|
-
|
|
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 = (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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.
|
|
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
|
|
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 "
|
|
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.
|
|
197
|
-
|
|
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>(
|
|
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>(
|
|
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
|
-
|
|
458
|
-
new
|
|
459
|
-
|
|
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
|
-
|
|
486
|
-
new
|
|
487
|
-
|
|
488
|
-
new Response("", {
|
|
489
|
-
status: 200,
|
|
490
|
-
})
|
|
491
|
-
);
|
|
494
|
+
mockFetch(
|
|
495
|
+
new Response("", {
|
|
496
|
+
status: 200,
|
|
492
497
|
})
|
|
493
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
541
|
+
mockFetch(new Response());
|
|
537
542
|
mockedOidcClient.getBearerToken = jest
|
|
538
543
|
.fn(getBearerToken)
|
|
539
544
|
.mockResolvedValueOnce({
|