UNPKG

10.2 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 "reflect-metadata";
23import {
24 StorageUtilityMock,
25 mockStorageUtility,
26} from "@inrupt/solid-client-authn-core";
27import { Response as NodeResponse } from "node-fetch";
28import ClientRegistrar from "../../../src/login/oidc/ClientRegistrar";
29import { IssuerConfigFetcherFetchConfigResponse } from "../../../src/login/oidc/__mocks__/IssuerConfigFetcher";
30
31/**
32 * Test for ClientRegistrar
33 */
34describe("ClientRegistrar", () => {
35 const defaultMocks = {
36 storage: StorageUtilityMock,
37 };
38 function getClientRegistrar(
39 mocks: Partial<typeof defaultMocks> = defaultMocks
40 ): ClientRegistrar {
41 return new ClientRegistrar(mocks.storage ?? defaultMocks.storage);
42 }
43
44 describe("getClient", () => {
45 it("properly performs dynamic registration", async () => {
46 const mockFetch = jest.fn().mockResolvedValueOnce(
47 /* eslint-disable camelcase */
48 (new NodeResponse(
49 JSON.stringify({
50 client_id: "abcd",
51 client_secret: "1234",
52 redirect_uris: ["https://example.com"],
53 })
54 ) as unknown) as Response
55 /* eslint-enable camelcase */
56 );
57 global.fetch = mockFetch;
58 const clientRegistrar = getClientRegistrar({
59 storage: mockStorageUtility({}),
60 });
61 const registrationUrl = "https://idp.com/register";
62 expect(
63 await clientRegistrar.getClient(
64 {
65 sessionId: "mySession",
66 redirectUrl: "https://example.com",
67 },
68 {
69 ...IssuerConfigFetcherFetchConfigResponse,
70 registrationEndpoint: registrationUrl,
71 }
72 )
73 ).toMatchObject({
74 clientId: "abcd",
75 clientSecret: "1234",
76 });
77 expect(mockFetch).toHaveBeenCalledWith(registrationUrl.toString(), {
78 method: "POST",
79 headers: {
80 "Content-Type": "application/json",
81 },
82 body: JSON.stringify({
83 /* eslint-disable camelcase */
84 application_type: "web",
85 redirect_uris: ["https://example.com"],
86 subject_type: "pairwise",
87 token_endpoint_auth_method: "client_secret_basic",
88 code_challenge_method: "S256",
89 /* eslint-enable camelcase */
90 }),
91 });
92 });
93
94 it("can register a public client without secret", async () => {
95 const mockFetch = jest.fn().mockResolvedValueOnce(
96 /* eslint-disable camelcase */
97 (new NodeResponse(
98 JSON.stringify({
99 client_id: "abcd",
100 redirect_uris: ["https://example.com"],
101 })
102 ) as unknown) as Response
103 /* eslint-enable camelcase */
104 );
105 global.fetch = mockFetch;
106 const clientRegistrar = getClientRegistrar({
107 storage: mockStorageUtility({}),
108 });
109 const registrationUrl = "https://idp.com/register";
110 const registeredClient = await clientRegistrar.getClient(
111 {
112 sessionId: "mySession",
113 redirectUrl: "https://example.com",
114 },
115 {
116 ...IssuerConfigFetcherFetchConfigResponse,
117 registrationEndpoint: registrationUrl,
118 }
119 );
120 expect(registeredClient.clientSecret).toBeUndefined();
121 });
122
123 it("Fails if there is not registration endpoint", async () => {
124 const clientRegistrar = getClientRegistrar({
125 storage: mockStorageUtility({}),
126 });
127 await expect(
128 clientRegistrar.getClient(
129 {
130 sessionId: "mySession",
131 redirectUrl: "https://example.com",
132 },
133 IssuerConfigFetcherFetchConfigResponse
134 )
135 ).rejects.toThrow(
136 "Dynamic Registration could not be completed because the issuer has no registration endpoint."
137 );
138 });
139
140 it("handles a failure to dynamically register elegantly", async () => {
141 const mockFetch = jest.fn().mockResolvedValueOnce(
142 /* eslint-disable camelcase */
143 (new NodeResponse('{"error":"bad stuff that\'s an error"}', {
144 status: 400,
145 }) as unknown) as Response
146 /* eslint-enable camelcase */
147 );
148 global.fetch = mockFetch;
149 const clientRegistrar = getClientRegistrar({
150 storage: mockStorageUtility({}),
151 });
152 const registrationUrl = "https://idp.com/register";
153 await expect(
154 clientRegistrar.getClient(
155 {
156 sessionId: "mySession",
157 redirectUrl: "https://example.com",
158 },
159 {
160 ...IssuerConfigFetcherFetchConfigResponse,
161 registrationEndpoint: registrationUrl,
162 }
163 )
164 ).rejects.toThrow(
165 "Client registration failed: [Error: Dynamic client registration failed: bad stuff that's an error - ]"
166 );
167 });
168
169 it("retrieves client id and secret from storage if they are present", async () => {
170 const clientRegistrar = getClientRegistrar({
171 storage: mockStorageUtility(
172 {
173 "solidClientAuthenticationUser:mySession": {
174 clientId: "an id",
175 clientSecret: "a secret",
176 },
177 },
178 false
179 ),
180 });
181 const client = await clientRegistrar.getClient(
182 {
183 sessionId: "mySession",
184 redirectUrl: "https://example.com",
185 },
186 {
187 ...IssuerConfigFetcherFetchConfigResponse,
188 }
189 );
190 expect(client.clientId).toEqual("an id");
191 expect(client.clientSecret).toEqual("a secret");
192 });
193
194 it("passes the registration token if provided", async () => {
195 const clientRegistrar = getClientRegistrar({
196 storage: mockStorageUtility({}),
197 });
198
199 const mockFetch = jest.fn().mockResolvedValueOnce(
200 /* eslint-disable camelcase */
201 (new NodeResponse(
202 JSON.stringify({
203 client_id: "abcd",
204 client_secret: "1234",
205 redirect_uris: ["https://example.com"],
206 })
207 ) as unknown) as Response
208 /* eslint-enable camelcase */
209 );
210 global.fetch = mockFetch;
211
212 await clientRegistrar.getClient(
213 {
214 sessionId: "mySession",
215 redirectUrl: "https://example.com",
216 registrationAccessToken: "some token",
217 },
218 {
219 ...IssuerConfigFetcherFetchConfigResponse,
220 registrationEndpoint: "https://some.issuer/register",
221 }
222 );
223
224 const registrationHeaders = mockFetch.mock.calls[0][1].headers as Record<
225 string,
226 string
227 >;
228 expect(registrationHeaders.Authorization).toEqual("Bearer some token");
229 });
230
231 it("retrieves the registration token from storage if present", async () => {
232 const clientRegistrar = getClientRegistrar({
233 storage: mockStorageUtility(
234 {
235 "solidClientAuthenticationUser:mySession": {
236 registrationAccessToken: "some token",
237 },
238 },
239 false
240 ),
241 });
242
243 const mockFetch = jest.fn().mockResolvedValueOnce(
244 /* eslint-disable camelcase */
245 (new NodeResponse(
246 JSON.stringify({
247 client_id: "abcd",
248 client_secret: "1234",
249 redirect_uris: ["https://example.com"],
250 })
251 ) as unknown) as Response
252 /* eslint-enable camelcase */
253 );
254 global.fetch = mockFetch;
255
256 await clientRegistrar.getClient(
257 {
258 sessionId: "mySession",
259 redirectUrl: "https://example.com",
260 },
261 {
262 ...IssuerConfigFetcherFetchConfigResponse,
263 registrationEndpoint: "https://some.issuer/register",
264 }
265 );
266
267 const registrationHeaders = mockFetch.mock.calls[0][1].headers as Record<
268 string,
269 string
270 >;
271 expect(registrationHeaders.Authorization).toEqual("Bearer some token");
272 });
273
274 it("saves dynamic registration information", async () => {
275 const mockFetch = jest.fn().mockResolvedValueOnce(
276 /* eslint-disable camelcase */
277 (new NodeResponse(
278 JSON.stringify({
279 client_id: "some id",
280 client_secret: "some secret",
281 redirect_uris: ["https://example.com"],
282 })
283 ) as unknown) as Response
284 /* eslint-enable camelcase */
285 );
286 global.fetch = mockFetch;
287 const myStorage = mockStorageUtility({});
288 const clientRegistrar = getClientRegistrar({
289 storage: myStorage,
290 });
291 const registrationUrl = "https://idp.com/register";
292
293 await clientRegistrar.getClient(
294 {
295 sessionId: "mySession",
296 redirectUrl: "https://example.com",
297 },
298 {
299 ...IssuerConfigFetcherFetchConfigResponse,
300 registrationEndpoint: registrationUrl,
301 }
302 );
303
304 await expect(
305 myStorage.getForUser("mySession", "clientId", { secure: false })
306 ).resolves.toEqual("some id");
307 await expect(
308 myStorage.getForUser("mySession", "clientSecret", { secure: false })
309 ).resolves.toEqual("some secret");
310 });
311 });
312});