UNPKG

22.8 kBPlain TextView Raw
1/*
2 * Copyright 2021 Inrupt Inc.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to use,
7 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8 * Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22import { jest, it, describe, expect } from "@jest/globals";
23import {
24 IIssuerConfig,
25 mockStorageUtility,
26 StorageUtility,
27 USER_SESSION_PREFIX,
28 mockStorage,
29} from "@inrupt/solid-client-authn-core";
30
31import { JWK, SignJWT, parseJwk } from "@inrupt/jose-legacy-modules";
32import { LoginHandlerMock } from "../src/login/__mocks__/LoginHandler";
33import {
34 RedirectHandlerMock,
35 RedirectHandlerResponse,
36} from "../src/login/oidc/redirectHandler/__mocks__/RedirectHandler";
37import { LogoutHandlerMock } from "../src/logout/__mocks__/LogoutHandler";
38import { mockSessionInfoManager } from "../src/sessionInfo/__mocks__/SessionInfoManager";
39import ClientAuthentication from "../src/ClientAuthentication";
40import { KEY_CURRENT_SESSION } from "../src/constant";
41import {
42 mockDefaultIssuerConfigFetcher,
43 mockIssuerConfigFetcher,
44} from "../src/login/oidc/__mocks__/IssuerConfigFetcher";
45import { LocalStorageMock } from "../src/storage/__mocks__/LocalStorage";
46
47jest.mock("@inrupt/solid-client-authn-core", () => {
48 const actualCoreModule = jest.requireActual(
49 "@inrupt/solid-client-authn-core"
50 /* eslint-disable @typescript-eslint/no-explicit-any */
51 ) as any;
52 return {
53 // We only want to fetch specific functions that result in a network fetch,
54 // but the rest of the module (e.g. the storage utilities) can be used unmocked.
55 ...actualCoreModule,
56 };
57});
58
59const mockJwk = (): JWK => {
60 return {
61 kty: "EC",
62 kid: "oOArcXxcwvsaG21jAx_D5CHr4BgVCzCEtlfmNFQtU0s",
63 alg: "ES256",
64 crv: "P-256",
65 x: "0dGe_s-urLhD3mpqYqmSXrqUZApVV5ZNxMJXg7Vp-2A",
66 y: "-oMe9gGkpfIrnJ0aiSUHMdjqYVm5ZrGCeQmRKoIIfj8",
67 d: "yR1bCsR7m4hjFCvWo8Jw3OfNR4aiYDAFbBD9nkudJKM",
68 };
69};
70
71const mockAnotherJwk = (): JWK => {
72 return {
73 kty: "EC",
74 kid: "oOArcXxcwvsaG21jAx_D5CHr4BgVCzCEtlfmNFQtU0s",
75 alg: "ES256",
76 crv: "P-256",
77 x: "0dGe_s-urLhD3mpqYqmSXriUZApVV5ZNxMJXg7Vp-2A",
78 y: "-oMe9gGkpfIr1J0aiSUHMdjqYVm5ZrGCeQmRKoIIfj8",
79 d: "yR1bCsR8m4hjFCvWo8Jw3OfNR4aiYDAFbBD9nkudJKM",
80 };
81};
82
83const mockIdTokenPayload = (
84 subject: string,
85 issuer: string,
86 audience: string
87): Record<string, string | number> => {
88 return {
89 sub: subject,
90 iss: issuer,
91 aud: audience,
92 exp: 1662266216,
93 iat: 1462266216,
94 };
95};
96
97type SessionStorageOptions = {
98 clientId: string;
99 issuer: string;
100};
101
102const mockSessionStorage = async (
103 sessionId: string,
104 idTokenPayload: Record<string, string | number> = {},
105 options: SessionStorageOptions = {
106 clientId: "https://some.app/registration",
107 issuer: "https://some.issuer",
108 }
109): Promise<StorageUtility> => {
110 return new StorageUtility(
111 mockStorage({
112 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
113 isLoggedIn: "true",
114 webId: "https://my.pod/profile#me",
115 },
116 }),
117 mockStorage({
118 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
119 idToken: await new SignJWT(idTokenPayload)
120 .setProtectedHeader({
121 alg: "ES256",
122 })
123 .setIssuedAt()
124 .sign(await parseJwk(mockJwk()), {}),
125 clientId: options.clientId,
126 issuer: options.issuer,
127 },
128 })
129 );
130};
131
132const mockLocalStorage = (stored: Record<string, string>) => {
133 // Kinda weird: `(window as any).localStorage = new LocalStorageMock(stored)` does
134 // not work as intended unless the following snippet is present in the test suite.
135 // On the other hand, only ever mocking localstorage with the following snippet
136 // works well.
137 Object.defineProperty(window, "localStorage", {
138 value: new LocalStorageMock(stored),
139 writable: true,
140 });
141};
142
143describe("ClientAuthentication", () => {
144 const defaultMocks = {
145 loginHandler: LoginHandlerMock,
146 redirectHandler: RedirectHandlerMock,
147 logoutHandler: LogoutHandlerMock,
148 sessionInfoManager: mockSessionInfoManager(mockStorageUtility({})),
149 issuerConfigFetcher: mockDefaultIssuerConfigFetcher(),
150 };
151
152 function getClientAuthentication(
153 mocks: Partial<typeof defaultMocks> = defaultMocks
154 ): ClientAuthentication {
155 return new ClientAuthentication(
156 mocks.loginHandler ?? defaultMocks.loginHandler,
157 mocks.redirectHandler ?? defaultMocks.redirectHandler,
158 mocks.logoutHandler ?? defaultMocks.logoutHandler,
159 mocks.sessionInfoManager ?? defaultMocks.sessionInfoManager,
160 mocks.issuerConfigFetcher ?? defaultMocks.issuerConfigFetcher
161 );
162 }
163
164 describe("login", () => {
165 it("calls login, and defaults to a DPoP token", async () => {
166 const clientAuthn = getClientAuthentication();
167 await clientAuthn.login({
168 sessionId: "mySession",
169 tokenType: "DPoP",
170 clientId: "coolApp",
171 redirectUrl: "https://coolapp.com/redirect",
172 oidcIssuer: "https://idp.com",
173 });
174 expect(defaultMocks.loginHandler.handle).toHaveBeenCalledWith({
175 sessionId: "mySession",
176 clientId: "coolApp",
177 redirectUrl: "https://coolapp.com/redirect",
178 oidcIssuer: "https://idp.com",
179 clientName: "coolApp",
180 clientSecret: undefined,
181 handleRedirect: undefined,
182 tokenType: "DPoP",
183 });
184 });
185
186 it("request a bearer token if specified", async () => {
187 const clientAuthn = getClientAuthentication();
188 await clientAuthn.login({
189 sessionId: "mySession",
190 clientId: "coolApp",
191 redirectUrl: "https://coolapp.com/redirect",
192 oidcIssuer: "https://idp.com",
193 tokenType: "Bearer",
194 });
195 expect(defaultMocks.loginHandler.handle).toHaveBeenCalledWith({
196 sessionId: "mySession",
197 clientId: "coolApp",
198 redirectUrl: "https://coolapp.com/redirect",
199 oidcIssuer: "https://idp.com",
200 clientName: "coolApp",
201 clientSecret: undefined,
202 handleRedirect: undefined,
203 tokenType: "Bearer",
204 });
205 });
206
207 it("should clear the local storage when logging in", async () => {
208 const nonEmptyStorage = mockStorageUtility({
209 someUser: { someKey: "someValue" },
210 });
211 await nonEmptyStorage.setForUser(
212 "someUser",
213 { someKey: "someValue" },
214 { secure: true }
215 );
216 const clientAuthn = getClientAuthentication({
217 sessionInfoManager: mockSessionInfoManager(nonEmptyStorage),
218 });
219 await clientAuthn.login({
220 sessionId: "someUser",
221 tokenType: "DPoP",
222 clientId: "coolApp",
223 clientName: "coolApp Name",
224 redirectUrl: "https://coolapp.com/redirect",
225 oidcIssuer: "https://idp.com",
226 });
227 await expect(
228 nonEmptyStorage.getForUser("someUser", "someKey", { secure: true })
229 ).resolves.toBeUndefined();
230 await expect(
231 nonEmptyStorage.getForUser("someUser", "someKey", { secure: false })
232 ).resolves.toBeUndefined();
233 // This test is only necessary until the key is stored safely
234 await expect(
235 nonEmptyStorage.get("clientKey", { secure: false })
236 ).resolves.toBeUndefined();
237 });
238 });
239
240 describe("fetch", () => {
241 it("calls fetch", async () => {
242 window.fetch = jest.fn();
243 const clientAuthn = getClientAuthentication();
244 await clientAuthn.fetch("https://html5zombo.com");
245 expect(window.fetch).toHaveBeenCalledWith(
246 "https://html5zombo.com",
247 undefined
248 );
249 });
250 });
251
252 describe("logout", () => {
253 it("reverts back to un-authenticated fetch on logout", async () => {
254 window.fetch = jest.fn();
255 // eslint-disable-next-line no-restricted-globals
256 history.replaceState = jest.fn();
257 const clientAuthn = getClientAuthentication();
258
259 const unauthFetch = clientAuthn.fetch;
260
261 const url =
262 "https://coolapp.com/redirect?state=userId&id_token=idToken&access_token=accessToken";
263 await clientAuthn.handleIncomingRedirect(url);
264
265 // Calling the redirect handler should give us an authenticated fetch.
266 expect(clientAuthn.fetch).not.toBe(unauthFetch);
267
268 await clientAuthn.logout("mySession");
269 const spyFetch = jest.spyOn(window, "fetch");
270 await clientAuthn.fetch("https://example.com", {
271 credentials: "omit",
272 });
273 // Calling logout should revert back to our un-authenticated fetch.
274 expect(clientAuthn.fetch).toBe(unauthFetch);
275 expect(spyFetch).toHaveBeenCalledWith("https://example.com", {
276 credentials: "omit",
277 });
278 });
279 });
280
281 describe("getAllSessionInfo", () => {
282 it("creates a session for the global user", async () => {
283 const clientAuthn = getClientAuthentication();
284 await expect(() => clientAuthn.getAllSessionInfo()).rejects.toThrow(
285 "Not implemented"
286 );
287 });
288 });
289
290 describe("getSessionInfo", () => {
291 it("creates a session for the global user", async () => {
292 const sessionInfo = {
293 isLoggedIn: "true",
294 sessionId: "mySession",
295 webId: "https://pod.com/profile/card#me",
296 };
297 const clientAuthn = getClientAuthentication({
298 sessionInfoManager: mockSessionInfoManager(
299 mockStorageUtility(
300 {
301 "solidClientAuthenticationUser:mySession": { ...sessionInfo },
302 },
303 true
304 )
305 ),
306 });
307 const session = await clientAuthn.getSessionInfo("mySession");
308 // isLoggedIn is stored as a string under the hood, but deserialized as a boolean
309 expect(session).toEqual({
310 ...sessionInfo,
311 isLoggedIn: true,
312 tokenType: "DPoP",
313 });
314 });
315 });
316
317 describe("handleIncomingRedirect", () => {
318 it("calls handle redirect", async () => {
319 // eslint-disable-next-line no-restricted-globals
320 history.replaceState = jest.fn();
321 const clientAuthn = getClientAuthentication();
322 const unauthFetch = clientAuthn.fetch;
323 const url =
324 "https://coolapp.com/redirect?state=userId&id_token=idToken&access_token=accessToken";
325 const redirectInfo = await clientAuthn.handleIncomingRedirect(url);
326
327 // Our injected mocked response may also contain internal-only data (for
328 // other tests), whereas our response from `handleIncomingRedirect()` can
329 // only contain publicly visible fields. So we need to explicitly check
330 // for individual fields (as opposed to just checking against
331 // entire-response-object-equality).
332 expect(redirectInfo?.sessionId).toEqual(
333 RedirectHandlerResponse.sessionId
334 );
335 expect(redirectInfo?.webId).toEqual(RedirectHandlerResponse.webId);
336 expect(redirectInfo?.isLoggedIn).toEqual(
337 RedirectHandlerResponse.isLoggedIn
338 );
339 expect(redirectInfo?.expirationDate).toEqual(
340 RedirectHandlerResponse.expirationDate
341 );
342 expect(defaultMocks.redirectHandler.handle).toHaveBeenCalledWith(url);
343
344 // Calling the redirect handler should have updated the fetch.
345 expect(clientAuthn.fetch).not.toBe(unauthFetch);
346 });
347
348 it("clears the current IRI from OAuth query parameters in the auth code flow", async () => {
349 // eslint-disable-next-line no-restricted-globals
350 history.replaceState = jest.fn();
351 const clientAuthn = getClientAuthentication();
352 const url =
353 "https://coolapp.com/redirect?state=someState&code=someAuthCode";
354 await clientAuthn.handleIncomingRedirect(url);
355 // eslint-disable-next-line no-restricted-globals
356 expect(history.replaceState).toHaveBeenCalledWith(
357 null,
358 "",
359 "https://coolapp.com/redirect"
360 );
361 });
362
363 it("clears the current IRI from OAuth query parameters in the implicit flow", async () => {
364 // eslint-disable-next-line no-restricted-globals
365 history.replaceState = jest.fn();
366 const clientAuthn = getClientAuthentication();
367 const url =
368 "https://coolapp.com/redirect?state=someState&id_token=idToken&access_token=accessToken";
369 await clientAuthn.handleIncomingRedirect(url);
370 // eslint-disable-next-line no-restricted-globals
371 expect(history.replaceState).toHaveBeenCalledWith(
372 null,
373 "",
374 "https://coolapp.com/redirect"
375 );
376 });
377
378 it("preserves non-OAuth query strings", async () => {
379 // eslint-disable-next-line no-restricted-globals
380 history.replaceState = jest.fn();
381 const clientAuthn = getClientAuthentication();
382 const url =
383 "https://coolapp.com/redirect?state=someState&code=someAuthCode&someQuery=someValue";
384 await clientAuthn.handleIncomingRedirect(url);
385 // eslint-disable-next-line no-restricted-globals
386 expect(history.replaceState).toHaveBeenCalledWith(
387 null,
388 "",
389 "https://coolapp.com/redirect?someQuery=someValue"
390 );
391 });
392 });
393
394 describe("getCurrentIssuer", () => {
395 // In the following describe block, (window as any) is used
396 // multiple types to override the window type definition and
397 // allow localStorage to be written.
398 /* eslint-disable @typescript-eslint/no-explicit-any */
399 it("returns null no current session is in storage", async () => {
400 const clientAuthn = getClientAuthentication({});
401
402 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
403 });
404
405 it("returns null if the current session has no stored issuer", async () => {
406 const sessionId = "mySession";
407 mockLocalStorage({
408 [KEY_CURRENT_SESSION]: sessionId,
409 });
410
411 const mockedStorage = new StorageUtility(
412 mockStorage({
413 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
414 isLoggedIn: "true",
415 },
416 }),
417 mockStorage({
418 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
419 clientId: "https://some.app/registration",
420 idToken: "some.id.token",
421 },
422 })
423 );
424 const clientAuthn = getClientAuthentication({
425 sessionInfoManager: mockSessionInfoManager(mockedStorage),
426 });
427
428 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
429 });
430
431 it("returns null if the current session has no stored ID token", async () => {
432 const sessionId = "mySession";
433 mockLocalStorage({
434 [KEY_CURRENT_SESSION]: sessionId,
435 });
436
437 const mockedStorage = new StorageUtility(
438 mockStorage({
439 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
440 isLoggedIn: "true",
441 issuer: "https://some.issuer",
442 },
443 }),
444 mockStorage({
445 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
446 clientId: "https://some.app/registration",
447 },
448 })
449 );
450 const clientAuthn = getClientAuthentication({
451 sessionInfoManager: mockSessionInfoManager(mockedStorage),
452 });
453
454 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
455 });
456
457 it("returns null if the current session has no stored client ID", async () => {
458 const sessionId = "mySession";
459 mockLocalStorage({
460 [KEY_CURRENT_SESSION]: sessionId,
461 });
462 const mockedStorage = new StorageUtility(
463 mockStorage({
464 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
465 isLoggedIn: "true",
466 issuer: "https://some.issuer",
467 },
468 }),
469 mockStorage({
470 [`${USER_SESSION_PREFIX}:${sessionId}`]: {
471 idToken: "some.id.token",
472 },
473 })
474 );
475 const clientAuthn = getClientAuthentication({
476 sessionInfoManager: mockSessionInfoManager(mockedStorage),
477 });
478
479 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
480 });
481
482 it("returns null if the issuer does not have a JWKS", async () => {
483 const sessionId = "mySession";
484 mockLocalStorage({
485 [KEY_CURRENT_SESSION]: sessionId,
486 });
487 const mockedIssuerConfig = mockIssuerConfigFetcher({} as IIssuerConfig);
488
489 const mockedStorage = await mockSessionStorage(sessionId);
490
491 const clientAuthn = getClientAuthentication({
492 issuerConfigFetcher: mockedIssuerConfig,
493 sessionInfoManager: mockSessionInfoManager(mockedStorage),
494 });
495
496 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
497 });
498
499 it("returns null if the issuer's JWKS isn't available", async () => {
500 const sessionId = "mySession";
501 mockLocalStorage({
502 [KEY_CURRENT_SESSION]: sessionId,
503 });
504 const mockedIssuerConfig = mockIssuerConfigFetcher({
505 jwksUri: "https://some.issuer/jwks",
506 } as IIssuerConfig);
507 const mockedStorage = await mockSessionStorage(sessionId);
508 const coreModule = jest.requireMock(
509 "@inrupt/solid-client-authn-core"
510 ) as jest.Mocked<any>;
511 coreModule.fetchJwks = jest.fn().mockRejectedValue("Not a valid JWK");
512
513 const clientAuthn = getClientAuthentication({
514 issuerConfigFetcher: mockedIssuerConfig,
515 sessionInfoManager: mockSessionInfoManager(mockedStorage),
516 });
517
518 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
519 });
520
521 it("returns null if the current issuer doesn't match the ID token's", async () => {
522 const sessionId = "mySession";
523 mockLocalStorage({
524 [KEY_CURRENT_SESSION]: sessionId,
525 });
526 const mockedIssuerConfig = mockIssuerConfigFetcher({
527 jwksUri: "https://some.issuer/jwks",
528 issuer: "https://some.issuer",
529 } as IIssuerConfig);
530 const mockedStorage = await mockSessionStorage(
531 sessionId,
532 mockIdTokenPayload(
533 "https://my.pod/profile#me",
534 // The ID token issuer
535 "https://some-other.issuer",
536 "https://some.app/registration"
537 ),
538 {
539 // The current issuer
540 issuer: "https://some.issuer",
541 clientId: "https://some.app/registration",
542 }
543 );
544 const coreModule = jest.requireMock(
545 "@inrupt/solid-client-authn-core"
546 ) as jest.Mocked<any>;
547 coreModule.fetchJwks = jest.fn(() => Promise.resolve(mockJwk()));
548
549 const clientAuthn = getClientAuthentication({
550 issuerConfigFetcher: mockedIssuerConfig,
551 sessionInfoManager: mockSessionInfoManager(mockedStorage),
552 });
553
554 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
555 });
556
557 it("returns null if the current client ID doesn't match the ID token audience", async () => {
558 const sessionId = "mySession";
559 mockLocalStorage({
560 [KEY_CURRENT_SESSION]: sessionId,
561 });
562 const mockedIssuerConfig = mockIssuerConfigFetcher({
563 jwksUri: "https://some.issuer/jwks",
564 } as IIssuerConfig);
565 const mockedStorage = await mockSessionStorage(
566 sessionId,
567 mockIdTokenPayload(
568 "https://my.pod/profile#me",
569 "https://some.issuer",
570 // The ID token audience
571 "https://some-other.app/registration"
572 ),
573 {
574 issuer: "https://some.issuer",
575 // The current client ID
576 clientId: "https://some.app/registration",
577 }
578 );
579 const coreModule = jest.requireMock(
580 "@inrupt/solid-client-authn-core"
581 ) as jest.Mocked<any>;
582 coreModule.fetchJwks = jest.fn(() => Promise.resolve(mockJwk()));
583
584 const clientAuthn = getClientAuthentication({
585 issuerConfigFetcher: mockedIssuerConfig,
586 sessionInfoManager: mockSessionInfoManager(mockedStorage),
587 });
588
589 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
590 });
591
592 it("returns null if the ID token isn't signed with the keys of the issuer", async () => {
593 const sessionId = "mySession";
594 mockLocalStorage({
595 [KEY_CURRENT_SESSION]: sessionId,
596 });
597 const mockedIssuerConfig = mockIssuerConfigFetcher({
598 jwksUri: "https://some.issuer/jwks",
599 } as IIssuerConfig);
600 const mockedStorage = await mockSessionStorage(
601 sessionId,
602 mockIdTokenPayload(
603 "https://my.pod/profile#me",
604 "https://some.issuer",
605 "https://some.app/registration"
606 )
607 );
608
609 const coreModule = jest.requireMock(
610 "@inrupt/solid-client-authn-core"
611 ) as jest.Mocked<any>;
612 coreModule.fetchJwks = jest.fn(() => Promise.resolve(mockAnotherJwk()));
613
614 const clientAuthn = getClientAuthentication({
615 issuerConfigFetcher: mockedIssuerConfig,
616 sessionInfoManager: mockSessionInfoManager(mockedStorage),
617 });
618
619 await expect(clientAuthn.validateCurrentSession()).resolves.toBeNull();
620 });
621 });
622
623 it("returns the issuer if the ID token is verified", async () => {
624 const sessionId = "mySession";
625 mockLocalStorage({
626 [KEY_CURRENT_SESSION]: sessionId,
627 });
628 const mockedIssuerConfig = mockIssuerConfigFetcher({
629 jwksUri: "https://some.issuer/jwks",
630 } as IIssuerConfig);
631 const mockedStorage = await mockSessionStorage(
632 sessionId,
633 mockIdTokenPayload(
634 "https://my.pod/profile#me",
635 "https://some.issuer",
636 "https://some.app/registration"
637 )
638 );
639 const coreModule = jest.requireMock(
640 "@inrupt/solid-client-authn-core"
641 ) as jest.Mocked<any>;
642 coreModule.fetchJwks = jest.fn(() => Promise.resolve(mockJwk()));
643
644 const clientAuthn = getClientAuthentication({
645 issuerConfigFetcher: mockedIssuerConfig,
646 sessionInfoManager: mockSessionInfoManager(mockedStorage),
647 });
648
649 await expect(clientAuthn.validateCurrentSession()).resolves.toStrictEqual(
650 expect.objectContaining({
651 issuer: "https://some.issuer",
652 clientAppId: "https://some.app/registration",
653 sessionId,
654 idToken: expect.anything(),
655 webId: "https://my.pod/profile#me",
656 })
657 );
658 });
659});