UNPKG

9.45 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 { Response as NodeResponse } from "node-fetch";
24import {
25 IIssuerConfig,
26 mockStorageUtility,
27} from "@inrupt/solid-client-authn-core";
28import { JSONWebKey } from "jose";
29import {
30 IssuerConfigFetcherMock,
31 IssuerConfigFetcherFetchConfigResponse,
32} from "../../../src/login/oidc/__mocks__/IssuerConfigFetcher";
33import TokenRequester from "../../../src/login/oidc/TokenRequester";
34import {
35 ClientRegistrarMock,
36 PublicClientRegistrarMock,
37} from "../../../src/login/oidc/__mocks__/ClientRegistrar";
38
39const mockJWK = {
40 kty: "EC",
41 kid: "oOArcXxcwvsaG21jAx_D5CHr4BgVCzCEtlfmNFQtU0s",
42 alg: "ES256",
43 crv: "P-256",
44 x: "0dGe_s-urLhD3mpqYqmSXrqUZApVV5ZNxMJXg7Vp-2A",
45 y: "-oMe9gGkpfIrnJ0aiSUHMdjqYVm5ZrGCeQmRKoIIfj8",
46 d: "yR1bCsR7m4hjFCvWo8Jw3OfNR4aiYDAFbBD9nkudJKM",
47};
48
49jest.mock("@inrupt/oidc-client-ext", () => {
50 return {
51 generateJwkForDpop: async (): Promise<typeof mockJWK> => mockJWK,
52 createDpopHeader: async (
53 _audience: URL,
54 _method: string,
55 _jwt: JSONWebKey
56 ): Promise<string> => "someToken",
57 decodeJwt: async (_jwt: string): Promise<Record<string, unknown>> => {
58 return {
59 sub: "https://some.webid",
60 };
61 },
62 };
63});
64
65describe("TokenRequester", () => {
66 const defaultMocks = {
67 storageUtility: mockStorageUtility({}),
68 issueConfigFetcher: IssuerConfigFetcherMock,
69 clientRegistrar: ClientRegistrarMock,
70 };
71
72 function getTokenRequester(
73 mocks: Partial<typeof defaultMocks> = defaultMocks
74 ): TokenRequester {
75 return new TokenRequester(
76 mocks.storageUtility ?? defaultMocks.storageUtility,
77 mocks.issueConfigFetcher ?? defaultMocks.issueConfigFetcher,
78 mocks.clientRegistrar ?? ClientRegistrarMock
79 );
80 }
81
82 const defaultReturnValues: {
83 storageIdp: string;
84 issuerConfig: IIssuerConfig;
85 responseBody: string;
86 jwt: Record<string, string>;
87 } = {
88 storageIdp: "https://idp.com",
89
90 issuerConfig: {
91 ...IssuerConfigFetcherFetchConfigResponse,
92 grantTypesSupported: ["refresh_token"],
93 },
94
95 responseBody: JSON.stringify({
96 /* eslint-disable camelcase */
97 id_token: "abcd",
98 access_token: "1234",
99 refresh_token: "!@#$",
100 /* eslint-enable camelcase */
101 }),
102
103 jwt: {
104 sub: "https://jackson.solid.community/profile/card#me",
105 },
106 };
107
108 async function setUpMockedReturnValues(
109 values: Partial<typeof defaultReturnValues>
110 ): Promise<void> {
111 await defaultMocks.storageUtility.setForUser("global", {
112 issuer: values.storageIdp ?? defaultReturnValues.storageIdp,
113 });
114
115 const issuerConfig =
116 values.issuerConfig ?? defaultReturnValues.issuerConfig;
117 defaultMocks.issueConfigFetcher.fetchConfig.mockResolvedValueOnce(
118 issuerConfig
119 );
120
121 const mockedFetch = jest
122 .fn()
123 .mockResolvedValueOnce(
124 (new NodeResponse(
125 values.responseBody ?? defaultReturnValues.responseBody
126 ) as unknown) as Response
127 );
128 window.fetch = mockedFetch;
129 }
130
131 it("Properly follows the refresh flow", async () => {
132 await setUpMockedReturnValues({});
133 const TokenRefresher = getTokenRequester({
134 clientRegistrar: PublicClientRegistrarMock,
135 });
136 /* eslint-disable camelcase */
137 await TokenRefresher.request("global", {
138 grant_type: "refresh_token",
139 refresh_token: "thisIsARefreshToken",
140 });
141 /* eslint-enable camelcase */
142 expect(window.fetch).toHaveBeenCalledWith(
143 IssuerConfigFetcherFetchConfigResponse.tokenEndpoint,
144 {
145 method: "POST",
146 headers: {
147 DPoP: "someToken",
148 "content-type": "application/x-www-form-urlencoded",
149 },
150 body:
151 "grant_type=refresh_token&refresh_token=thisIsARefreshToken&client_id=abcde",
152 }
153 );
154 });
155
156 it("Adds an authorization header if a client secret is present", async () => {
157 const TokenRefresher = getTokenRequester();
158 window.fetch = jest.fn().mockResolvedValueOnce(
159 new NodeResponse(
160 JSON.stringify({
161 /* eslint-disable camelcase */
162 access_token:
163 "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJodHRwczovL3NvbWUucG9kL3dlYmlkI21lIiwianRpIjoiMjFiNDhlOTgtNzA4YS00Y2UzLTk2NmMtYTk2M2UwMDM2ZDBiIiwiaWF0IjoxNTk1ODQxODAxLCJleHAiOjE1OTU4NDU0MDF9.MGqDiny3pzhlSOJ52cJWJA84J47b9p5kkuuJVPB-dVg",
164 id_token: "myIdToken",
165 refresh_token: "myResfreshToken",
166 /* eslint-disable camelcase */
167 })
168 )
169 );
170 /* eslint-disable camelcase */
171 await TokenRefresher.request("global", {
172 grant_type: "refresh_token",
173 refresh_token: "thisIsARefreshToken",
174 });
175 /* eslint-enable camelcase */
176 expect(window.fetch).toHaveBeenCalledWith(
177 IssuerConfigFetcherFetchConfigResponse.tokenEndpoint,
178 {
179 method: "POST",
180 headers: {
181 DPoP: "someToken",
182 Authorization: "Basic YWJjZGU6MTIzNDU=",
183 "content-type": "application/x-www-form-urlencoded",
184 },
185 body:
186 "grant_type=refresh_token&refresh_token=thisIsARefreshToken&client_id=abcde",
187 }
188 );
189 });
190
191 it("Fails elegantly if the idp returns a bad value", async () => {
192 await setUpMockedReturnValues({
193 responseBody: JSON.stringify({
194 // eslint-disable-next-line camelcase
195 id_token: "ohNoThereIsNoAccessToken",
196 }),
197 });
198 const TokenRefresher = getTokenRequester();
199 await expect(
200 /* eslint-disable camelcase */
201 TokenRefresher.request("global", {
202 grant_type: "refresh_token",
203 refresh_token: "thisIsARefreshToken",
204 })
205 /* eslint-enable camelcase */
206 ).rejects.toThrow("IDP token route returned an invalid response.");
207 });
208
209 it("Fails elegantly if the issuer does not support refresh tokens", async () => {
210 await setUpMockedReturnValues({
211 issuerConfig: {
212 ...IssuerConfigFetcherFetchConfigResponse,
213 // eslint-disable-next-line camelcase
214 grantTypesSupported: ["id_token"],
215 },
216 });
217 const TokenRefresher = getTokenRequester();
218 await expect(
219 /* eslint-disable camelcase */
220 TokenRefresher.request("global", {
221 grant_type: "refresh_token",
222 refresh_token: "thisIsARefreshToken",
223 })
224 /* eslint-enable camelcase */
225 ).rejects.toThrow(
226 "The issuer [https://idp.com] does not support the [refresh_token] grant"
227 );
228 });
229
230 it("Fails elegantly if the issuer does not have a token endpoint", async () => {
231 const mockIssuerConfig = {
232 ...IssuerConfigFetcherFetchConfigResponse,
233 tokenEndpoint: null,
234 // eslint-disable-next-line camelcase
235 grantTypesSupported: ["refresh_token"],
236 };
237 const mockIssuerConfigFetcher = defaultMocks.issueConfigFetcher;
238 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
239 // @ts-ignore This is ignored to test an edge case: tokenEndpoint should not be null
240 mockIssuerConfigFetcher.fetchConfig.mockResolvedValueOnce(mockIssuerConfig);
241
242 const TokenRefresher = getTokenRequester({
243 issueConfigFetcher: mockIssuerConfigFetcher,
244 });
245 await expect(
246 /* eslint-disable camelcase */
247 TokenRefresher.request("global", {
248 grant_type: "refresh_token",
249 refresh_token: "thisIsARefreshToken",
250 })
251 /* eslint-enable camelcase */
252 // TODO: Should be This issuer https://idp.com does not have a token endpoint"
253 // Figure out why the test suite shuffles the issuer.
254 ).rejects.toThrow("does not have a token endpoint");
255 });
256
257 // This test fails with the current mock, but since the whole tokenrequester class is
258 // going to be removed soon, it's not a priority to fix this now.
259 it.skip("Fails elegantly if the access token does not have a sub claim", async () => {
260 await setUpMockedReturnValues({
261 jwt: {
262 iss: "https://idp.com",
263 },
264 });
265 const TokenRefresher = getTokenRequester();
266 await expect(
267 /* eslint-disable camelcase */
268 TokenRefresher.request("global", {
269 grant_type: "refresh_token",
270 refresh_token: "thisIsARefreshToken",
271 })
272 /* eslint-enable camelcase */
273 ).rejects.toThrow(
274 "The Authorization Server returned a bad token (i.e. when decoded we did not find the required 'sub' claim)."
275 );
276 });
277});