@telicent-oss/fe-auth-lib 1.0.1 → 1.0.2-TELFE-1477.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/package.json +3 -3
- package/src/AuthServerOAuth2Client.d.ts +12 -15
- package/src/AuthServerOAuth2Client.js +22 -5
- package/src/__tests__/callback.failures.test.ts +285 -0
- package/src/__tests__/callback.success.test.ts +410 -0
- package/src/__tests__/core.failures.test.ts +122 -0
- package/src/__tests__/core.success.test.ts +196 -0
- package/src/__tests__/env.success.test.ts +17 -0
- package/src/__tests__/logout.success.test.ts +151 -0
- package/src/__tests__/methods/base64URLEncode.success.test.ts +39 -0
- package/src/__tests__/methods/generateCodeChallenge.success.test.ts +43 -0
- package/src/__tests__/methods/generateCodeVerifier.success.test.ts +43 -0
- package/src/__tests__/methods/generateNonce.success.test.ts +43 -0
- package/src/__tests__/methods/generateState.success.test.ts +43 -0
- package/src/__tests__/methods/getCsrfToken.failures.test.ts +54 -0
- package/src/__tests__/methods/getCsrfToken.success.test.ts +45 -0
- package/src/__tests__/methods/getRawIdToken.success.test.ts +39 -0
- package/src/__tests__/methods/getUserInfo.failures.test.ts +153 -0
- package/src/__tests__/methods/getUserInfo.success.test.ts +84 -0
- package/src/__tests__/methods/isAuthenticated.failures.test.ts +62 -0
- package/src/__tests__/methods/isAuthenticated.success.test.ts +84 -0
- package/src/__tests__/methods/isIdTokenExpired.failures.test.ts +77 -0
- package/src/__tests__/methods/isIdTokenExpired.success.test.ts +49 -0
- package/src/__tests__/methods/validateIdToken.failures.test.ts +177 -0
- package/src/__tests__/methods/validateIdToken.success.test.ts +55 -0
- package/src/__tests__/methods/validateIdTokenForRecovery.failures.test.ts +121 -0
- package/src/__tests__/methods/validateIdTokenForRecovery.success.test.ts +49 -0
- package/src/__tests__/popup.success.test.ts +277 -0
- package/src/__tests__/request-helpers.failures.test.ts +143 -0
- package/src/__tests__/request-helpers.success.test.ts +137 -0
- package/src/__tests__/schema-loading.failures.test.ts +44 -0
- package/src/__tests__/schema-loading.success.test.ts +106 -0
- package/src/__tests__/state.success.test.ts +217 -0
- package/src/__tests__/test-utils.node.success.test.ts +16 -0
- package/src/__tests__/test-utils.success.test.ts +188 -0
- package/src/__tests__/test-utils.ts +203 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import AuthServerOAuth2Client, {
|
|
2
|
+
AuthServerOAuth2ClientConfig,
|
|
3
|
+
} from "../AuthServerOAuth2Client";
|
|
4
|
+
import {
|
|
5
|
+
buildJwt,
|
|
6
|
+
installTestEnv,
|
|
7
|
+
mockPkceValues,
|
|
8
|
+
resetTestEnv,
|
|
9
|
+
setWindowLocation,
|
|
10
|
+
} from "./test-utils";
|
|
11
|
+
|
|
12
|
+
const createConfig = (
|
|
13
|
+
overrides: Partial<AuthServerOAuth2ClientConfig> = {}
|
|
14
|
+
): AuthServerOAuth2ClientConfig => ({
|
|
15
|
+
clientId: "client-1",
|
|
16
|
+
authServerUrl: "http://auth.telicent.localhost",
|
|
17
|
+
redirectUri: "http://app.telicent.localhost/callback",
|
|
18
|
+
popupRedirectUri: "http://app.telicent.localhost/popup",
|
|
19
|
+
scope: "openid profile",
|
|
20
|
+
onLogout: jest.fn(),
|
|
21
|
+
...overrides,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const createFetchResponse = (options: {
|
|
25
|
+
ok?: boolean;
|
|
26
|
+
status?: number;
|
|
27
|
+
jsonData?: unknown;
|
|
28
|
+
textData?: string;
|
|
29
|
+
}): Response =>
|
|
30
|
+
({
|
|
31
|
+
ok: options.ok ?? true,
|
|
32
|
+
status: options.status ?? 200,
|
|
33
|
+
json: jest.fn().mockResolvedValue(options.jsonData ?? {}),
|
|
34
|
+
text: jest.fn().mockResolvedValue(options.textData ?? ""),
|
|
35
|
+
} as unknown as Response);
|
|
36
|
+
|
|
37
|
+
const matchedNonce = "✅ MATCHED nonce";
|
|
38
|
+
const mismatchedNonces = {
|
|
39
|
+
stored: "❌ 🍎 MIS-matched nonce",
|
|
40
|
+
token: "❌ 🍌 MIS-matched nonce",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("happy path - handleCallback stores session and id token", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
installTestEnv();
|
|
46
|
+
setWindowLocation("http://app.telicent.localhost/home");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
resetTestEnv();
|
|
51
|
+
jest.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("stores session and id token for cross-domain session", async () => {
|
|
55
|
+
jest.useFakeTimers();
|
|
56
|
+
const now = 1_700_000_000_000;
|
|
57
|
+
jest.spyOn(Date, "now").mockReturnValue(now);
|
|
58
|
+
|
|
59
|
+
const client = new AuthServerOAuth2Client(
|
|
60
|
+
createConfig({ authServerUrl: "https://auth.telicent.io" })
|
|
61
|
+
);
|
|
62
|
+
mockPkceValues(client, {
|
|
63
|
+
state: "ABC_state_ABC",
|
|
64
|
+
nonce: "ABC_nonce_ABC",
|
|
65
|
+
codeVerifier: "ABC_codeVerifier_ABC",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
sessionStorage.setItem("oauth_state", "ABC_state_ABC");
|
|
69
|
+
sessionStorage.setItem("oauth_nonce", "ABC_nonce_ABC");
|
|
70
|
+
sessionStorage.setItem("oauth_code_verifier", "ABC_codeVerifier_ABC");
|
|
71
|
+
sessionStorage.setItem(
|
|
72
|
+
"oauth_redirect_uri",
|
|
73
|
+
"http://app.telicent.localhost/callback"
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const idToken = buildJwt({
|
|
77
|
+
sub: "user-1",
|
|
78
|
+
aud: "client-1",
|
|
79
|
+
exp: Math.floor(now / 1000) + 300,
|
|
80
|
+
iat: Math.floor(now / 1000),
|
|
81
|
+
nonce: "ABC_nonce_ABC",
|
|
82
|
+
email: "user@example.com",
|
|
83
|
+
preferred_name: "User One",
|
|
84
|
+
iss: "https://auth.telicent.io",
|
|
85
|
+
jti: "id-1",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const fetchMock = jest.fn();
|
|
89
|
+
fetchMock
|
|
90
|
+
.mockResolvedValueOnce(
|
|
91
|
+
createFetchResponse({
|
|
92
|
+
jsonData: {
|
|
93
|
+
isCrossDomain: true,
|
|
94
|
+
sessionToken: "SESSION_123",
|
|
95
|
+
user: "user-1",
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
.mockResolvedValueOnce(
|
|
100
|
+
createFetchResponse({
|
|
101
|
+
jsonData: { id_token: idToken },
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
globalThis.fetch = fetchMock;
|
|
106
|
+
|
|
107
|
+
const promise = client.handleCallback({
|
|
108
|
+
code: "CODE_123",
|
|
109
|
+
state: "ABC_state_ABC",
|
|
110
|
+
});
|
|
111
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
112
|
+
const sessionData = await promise;
|
|
113
|
+
|
|
114
|
+
expect({
|
|
115
|
+
sessionData,
|
|
116
|
+
storage: {
|
|
117
|
+
authSessionId: sessionStorage.getItem("auth_session_id"),
|
|
118
|
+
authIdToken: sessionStorage.getItem("auth_id_token"),
|
|
119
|
+
oauthState: sessionStorage.getItem("oauth_state"),
|
|
120
|
+
oauthCodeVerifier: sessionStorage.getItem("oauth_code_verifier"),
|
|
121
|
+
oauthNonce: sessionStorage.getItem("oauth_nonce"),
|
|
122
|
+
},
|
|
123
|
+
fetchCalls: fetchMock.mock.calls.map((call) => call[0]),
|
|
124
|
+
}).toMatchInlineSnapshot(`
|
|
125
|
+
{
|
|
126
|
+
"fetchCalls": [
|
|
127
|
+
"https://auth.telicent.io/oauth2/token",
|
|
128
|
+
"https://auth.telicent.io/session/idtoken",
|
|
129
|
+
],
|
|
130
|
+
"sessionData": {
|
|
131
|
+
"isCrossDomain": true,
|
|
132
|
+
"sessionToken": "SESSION_123",
|
|
133
|
+
"user": "user-1",
|
|
134
|
+
},
|
|
135
|
+
"storage": {
|
|
136
|
+
"authIdToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTEiLCJhdWQiOiJjbGllbnQtMSIsImV4cCI6MTcwMDAwMDMwMCwiaWF0IjoxNzAwMDAwMDAwLCJub25jZSI6IkFCQ19ub25jZV9BQkMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJwcmVmZXJyZWRfbmFtZSI6IlVzZXIgT25lIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLnRlbGljZW50LmlvIiwianRpIjoiaWQtMSJ9.",
|
|
137
|
+
"authSessionId": "SESSION_123",
|
|
138
|
+
"oauthCodeVerifier": null,
|
|
139
|
+
"oauthNonce": null,
|
|
140
|
+
"oauthState": null,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
`);
|
|
144
|
+
|
|
145
|
+
(Date.now as jest.Mock).mockRestore();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("completes callback when id token response is not ok (500 status)", async () => {
|
|
149
|
+
jest.useFakeTimers();
|
|
150
|
+
|
|
151
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
152
|
+
|
|
153
|
+
mockPkceValues(client, {
|
|
154
|
+
state: "ABC_state_ABC",
|
|
155
|
+
nonce: matchedNonce,
|
|
156
|
+
codeVerifier: "ABC_codeVerifier_ABC",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
sessionStorage.setItem("oauth_state", "ABC_state_ABC");
|
|
160
|
+
sessionStorage.setItem("oauth_nonce", matchedNonce);
|
|
161
|
+
sessionStorage.setItem("oauth_code_verifier", "ABC_codeVerifier_ABC");
|
|
162
|
+
sessionStorage.setItem(
|
|
163
|
+
"oauth_redirect_uri",
|
|
164
|
+
"http://app.telicent.localhost/callback"
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const fetchMock = jest.fn();
|
|
168
|
+
fetchMock
|
|
169
|
+
.mockResolvedValueOnce(
|
|
170
|
+
createFetchResponse({
|
|
171
|
+
jsonData: { isCrossDomain: false, user: "user-1" },
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
.mockResolvedValueOnce(
|
|
175
|
+
createFetchResponse({
|
|
176
|
+
ok: false,
|
|
177
|
+
status: 500,
|
|
178
|
+
jsonData: { error: "server_error" },
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
globalThis.fetch = fetchMock;
|
|
182
|
+
|
|
183
|
+
const promise = client.handleCallback({
|
|
184
|
+
code: "CODE_123",
|
|
185
|
+
state: "ABC_state_ABC",
|
|
186
|
+
});
|
|
187
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
188
|
+
const sessionData = await promise;
|
|
189
|
+
|
|
190
|
+
expect({
|
|
191
|
+
sessionData,
|
|
192
|
+
storage: {
|
|
193
|
+
authSessionId: sessionStorage.getItem("auth_session_id"),
|
|
194
|
+
authIdToken: sessionStorage.getItem("auth_id_token"),
|
|
195
|
+
oauthState: sessionStorage.getItem("oauth_state"),
|
|
196
|
+
oauthCodeVerifier: sessionStorage.getItem("oauth_code_verifier"),
|
|
197
|
+
oauthNonce: sessionStorage.getItem("oauth_nonce"),
|
|
198
|
+
},
|
|
199
|
+
}).toMatchInlineSnapshot(`
|
|
200
|
+
{
|
|
201
|
+
"sessionData": {
|
|
202
|
+
"isCrossDomain": false,
|
|
203
|
+
"user": "user-1",
|
|
204
|
+
},
|
|
205
|
+
"storage": {
|
|
206
|
+
"authIdToken": null,
|
|
207
|
+
"authSessionId": null,
|
|
208
|
+
"oauthCodeVerifier": null,
|
|
209
|
+
"oauthNonce": "✅ MATCHED nonce",
|
|
210
|
+
"oauthState": null,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("stores id token without cross-domain session id", async () => {
|
|
217
|
+
jest.useFakeTimers();
|
|
218
|
+
const now = 1_700_000_100_000;
|
|
219
|
+
jest.spyOn(Date, "now").mockReturnValue(now);
|
|
220
|
+
|
|
221
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
222
|
+
mockPkceValues(client, {
|
|
223
|
+
state: "ABC_state_ABC",
|
|
224
|
+
nonce: "ABC_nonce_ABC",
|
|
225
|
+
codeVerifier: "ABC_codeVerifier_ABC",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
sessionStorage.setItem("oauth_state", "ABC_state_ABC");
|
|
229
|
+
sessionStorage.setItem("oauth_nonce", "ABC_nonce_ABC");
|
|
230
|
+
sessionStorage.setItem("oauth_code_verifier", "ABC_codeVerifier_ABC");
|
|
231
|
+
sessionStorage.setItem(
|
|
232
|
+
"oauth_redirect_uri",
|
|
233
|
+
"http://app.telicent.localhost/callback"
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const idToken = buildJwt({
|
|
237
|
+
sub: "user-2",
|
|
238
|
+
aud: "client-1",
|
|
239
|
+
exp: Math.floor(now / 1000) + 300,
|
|
240
|
+
iat: Math.floor(now / 1000),
|
|
241
|
+
nonce: "ABC_nonce_ABC",
|
|
242
|
+
email: "user2@example.com",
|
|
243
|
+
preferred_name: "User Two",
|
|
244
|
+
iss: "http://auth.telicent.localhost",
|
|
245
|
+
jti: "id-2",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const fetchMock = jest.fn();
|
|
249
|
+
fetchMock
|
|
250
|
+
.mockResolvedValueOnce(
|
|
251
|
+
createFetchResponse({
|
|
252
|
+
jsonData: {
|
|
253
|
+
isCrossDomain: true,
|
|
254
|
+
sessionToken: "SESSION_456",
|
|
255
|
+
user: "user-2",
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
)
|
|
259
|
+
.mockResolvedValueOnce(
|
|
260
|
+
createFetchResponse({
|
|
261
|
+
jsonData: { id_token: idToken },
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
globalThis.fetch = fetchMock;
|
|
265
|
+
|
|
266
|
+
const promise = client.handleCallback({
|
|
267
|
+
code: "CODE_456",
|
|
268
|
+
state: "ABC_state_ABC",
|
|
269
|
+
});
|
|
270
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
271
|
+
const sessionData = await promise;
|
|
272
|
+
|
|
273
|
+
expect({
|
|
274
|
+
sessionData,
|
|
275
|
+
storage: {
|
|
276
|
+
authSessionId: sessionStorage.getItem("auth_session_id"),
|
|
277
|
+
authIdToken: sessionStorage.getItem("auth_id_token"),
|
|
278
|
+
},
|
|
279
|
+
}).toMatchInlineSnapshot(`
|
|
280
|
+
{
|
|
281
|
+
"sessionData": {
|
|
282
|
+
"isCrossDomain": true,
|
|
283
|
+
"sessionToken": "SESSION_456",
|
|
284
|
+
"user": "user-2",
|
|
285
|
+
},
|
|
286
|
+
"storage": {
|
|
287
|
+
"authIdToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTIiLCJhdWQiOiJjbGllbnQtMSIsImV4cCI6MTcwMDAwMDQwMCwiaWF0IjoxNzAwMDAwMTAwLCJub25jZSI6IkFCQ19ub25jZV9BQkMiLCJlbWFpbCI6InVzZXIyQGV4YW1wbGUuY29tIiwicHJlZmVycmVkX25hbWUiOiJVc2VyIFR3byIsImlzcyI6Imh0dHA6Ly9hdXRoLnRlbGljZW50LmxvY2FsaG9zdCIsImp0aSI6ImlkLTIifQ.",
|
|
288
|
+
"authSessionId": null,
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
`);
|
|
292
|
+
|
|
293
|
+
(Date.now as jest.Mock).mockRestore();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("warns when id token validation fails (nonce mismatch)", async () => {
|
|
297
|
+
jest.useFakeTimers();
|
|
298
|
+
const now = 1_700_000_200_000;
|
|
299
|
+
jest.spyOn(Date, "now").mockReturnValue(now);
|
|
300
|
+
|
|
301
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
302
|
+
mockPkceValues(client, {
|
|
303
|
+
state: "ABC_state_ABC",
|
|
304
|
+
nonce: mismatchedNonces.stored,
|
|
305
|
+
codeVerifier: "ABC_codeVerifier_ABC",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
sessionStorage.setItem("oauth_state", "ABC_state_ABC");
|
|
309
|
+
sessionStorage.setItem("oauth_nonce", mismatchedNonces.stored);
|
|
310
|
+
sessionStorage.setItem("oauth_code_verifier", "ABC_codeVerifier_ABC");
|
|
311
|
+
sessionStorage.setItem(
|
|
312
|
+
"oauth_redirect_uri",
|
|
313
|
+
"http://app.telicent.localhost/callback"
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const nonceMismatchIdToken = buildJwt({
|
|
317
|
+
sub: "user-3",
|
|
318
|
+
aud: "client-1",
|
|
319
|
+
exp: Math.floor(now / 1000) + 300,
|
|
320
|
+
iat: Math.floor(now / 1000),
|
|
321
|
+
nonce: mismatchedNonces.token,
|
|
322
|
+
email: "user3@example.com",
|
|
323
|
+
preferred_name: "User Three",
|
|
324
|
+
iss: "http://auth.telicent.localhost",
|
|
325
|
+
jti: "id-3",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const fetchMock = jest.fn().mockResolvedValue(
|
|
329
|
+
createFetchResponse({
|
|
330
|
+
jsonData: { isCrossDomain: false, user: "user-3" },
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
globalThis.fetch = fetchMock;
|
|
334
|
+
jest
|
|
335
|
+
.spyOn(client, "makeAuthenticatedRequest")
|
|
336
|
+
.mockResolvedValue(
|
|
337
|
+
createFetchResponse({ jsonData: { id_token: nonceMismatchIdToken } })
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const promise = client.handleCallback({
|
|
341
|
+
code: "CODE_789",
|
|
342
|
+
state: "ABC_state_ABC",
|
|
343
|
+
});
|
|
344
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
345
|
+
await promise;
|
|
346
|
+
|
|
347
|
+
expect({
|
|
348
|
+
reason: mismatchedNonces,
|
|
349
|
+
warnings: (console.warn as jest.Mock).mock.calls.map((call) => call[0]),
|
|
350
|
+
}).toMatchInlineSnapshot(`
|
|
351
|
+
{
|
|
352
|
+
"reason": {
|
|
353
|
+
"stored": "❌ 🍎 MIS-matched nonce",
|
|
354
|
+
"token": "❌ 🍌 MIS-matched nonce",
|
|
355
|
+
},
|
|
356
|
+
"warnings": [
|
|
357
|
+
"ID token validation failed, but continuing with callback",
|
|
358
|
+
],
|
|
359
|
+
}
|
|
360
|
+
`);
|
|
361
|
+
|
|
362
|
+
(Date.now as jest.Mock).mockRestore();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("warns when id token retrieval throws", async () => {
|
|
366
|
+
jest.useFakeTimers();
|
|
367
|
+
|
|
368
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
369
|
+
mockPkceValues(client, {
|
|
370
|
+
state: "ABC_state_ABC",
|
|
371
|
+
nonce: "ABC_nonce_ABC",
|
|
372
|
+
codeVerifier: "ABC_codeVerifier_ABC",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
sessionStorage.setItem("oauth_state", "ABC_state_ABC");
|
|
376
|
+
sessionStorage.setItem("oauth_nonce", "ABC_nonce_ABC");
|
|
377
|
+
sessionStorage.setItem("oauth_code_verifier", "ABC_codeVerifier_ABC");
|
|
378
|
+
sessionStorage.setItem(
|
|
379
|
+
"oauth_redirect_uri",
|
|
380
|
+
"http://app.telicent.localhost/callback"
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const fetchMock = jest.fn().mockResolvedValue(
|
|
384
|
+
createFetchResponse({
|
|
385
|
+
jsonData: { isCrossDomain: false, user: "user-4" },
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
globalThis.fetch = fetchMock;
|
|
389
|
+
jest
|
|
390
|
+
.spyOn(client, "makeAuthenticatedRequest")
|
|
391
|
+
.mockRejectedValue(new Error("id token fetch failed"));
|
|
392
|
+
|
|
393
|
+
const promise = client.handleCallback({
|
|
394
|
+
code: "CODE_456",
|
|
395
|
+
state: "ABC_state_ABC",
|
|
396
|
+
});
|
|
397
|
+
await jest.advanceTimersByTimeAsync(100);
|
|
398
|
+
await promise;
|
|
399
|
+
|
|
400
|
+
expect({
|
|
401
|
+
warnings: (console.warn as jest.Mock).mock.calls.map((call) => call[0]),
|
|
402
|
+
}).toMatchInlineSnapshot(`
|
|
403
|
+
{
|
|
404
|
+
"warnings": [
|
|
405
|
+
"Error retrieving ID token during callback, but continuing:",
|
|
406
|
+
],
|
|
407
|
+
}
|
|
408
|
+
`);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import AuthServerOAuth2Client, {
|
|
2
|
+
AuthServerOAuth2ClientConfig,
|
|
3
|
+
} from "../AuthServerOAuth2Client";
|
|
4
|
+
import { installTestEnv, resetTestEnv, setWindowLocation } from "./test-utils";
|
|
5
|
+
|
|
6
|
+
const createConfig = (
|
|
7
|
+
overrides: Partial<AuthServerOAuth2ClientConfig> = {}
|
|
8
|
+
): AuthServerOAuth2ClientConfig => ({
|
|
9
|
+
clientId: "client-1",
|
|
10
|
+
authServerUrl: "http://auth.telicent.localhost",
|
|
11
|
+
redirectUri: "http://app.telicent.localhost/callback",
|
|
12
|
+
popupRedirectUri: "http://app.telicent.localhost/popup",
|
|
13
|
+
scope: "openid profile",
|
|
14
|
+
onLogout: jest.fn(),
|
|
15
|
+
...overrides,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("failure path - validation failures and edge handling", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
installTestEnv();
|
|
21
|
+
setWindowLocation("http://app.telicent.localhost/home");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
resetTestEnv();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("throws on invalid config and logs details", () => {
|
|
29
|
+
let error: Error | null = null;
|
|
30
|
+
try {
|
|
31
|
+
new AuthServerOAuth2Client(createConfig({ authServerUrl: "not-a-url" }));
|
|
32
|
+
} catch (caught) {
|
|
33
|
+
error = caught as Error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const errorCalls = (console.error as jest.Mock).mock.calls;
|
|
37
|
+
|
|
38
|
+
expect({
|
|
39
|
+
error: error?.message,
|
|
40
|
+
consoleError: errorCalls.map((call) => call[0]),
|
|
41
|
+
}).toMatchInlineSnapshot(`
|
|
42
|
+
{
|
|
43
|
+
"consoleError": [
|
|
44
|
+
"❌ Invalid AuthServerOAuth2Client configuration:",
|
|
45
|
+
],
|
|
46
|
+
"error": "Invalid AuthServerOAuth2Client configuration: [
|
|
47
|
+
{
|
|
48
|
+
"validation": "url",
|
|
49
|
+
"code": "invalid_string",
|
|
50
|
+
"message": "Invalid url",
|
|
51
|
+
"path": [
|
|
52
|
+
"authServerUrl"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]",
|
|
56
|
+
}
|
|
57
|
+
`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns early when config is undefined", () => {
|
|
61
|
+
const client = new (AuthServerOAuth2Client as unknown as new (
|
|
62
|
+
config?: AuthServerOAuth2ClientConfig
|
|
63
|
+
) => AuthServerOAuth2Client)(undefined);
|
|
64
|
+
|
|
65
|
+
const warnCalls = (console.warn as jest.Mock).mock.calls;
|
|
66
|
+
|
|
67
|
+
expect({
|
|
68
|
+
config: (client as AuthServerOAuth2Client).config,
|
|
69
|
+
isCrossDomain: (client as AuthServerOAuth2Client).isCrossDomain,
|
|
70
|
+
consoleWarn: warnCalls.map((call) => call[0]),
|
|
71
|
+
}).toMatchInlineSnapshot(`
|
|
72
|
+
{
|
|
73
|
+
"config": undefined,
|
|
74
|
+
"consoleWarn": [
|
|
75
|
+
"⚠️ AuthServerOAuth2Client instantiated with undefined config",
|
|
76
|
+
],
|
|
77
|
+
"isCrossDomain": undefined,
|
|
78
|
+
}
|
|
79
|
+
`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("defaults to cross-domain when window origin is invalid", () => {
|
|
83
|
+
Object.defineProperty(window, "location", {
|
|
84
|
+
value: { origin: "not-a-url" },
|
|
85
|
+
writable: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
89
|
+
const warnCalls = (console.warn as jest.Mock).mock.calls;
|
|
90
|
+
|
|
91
|
+
expect({
|
|
92
|
+
isCrossDomain: client.isCrossDomain,
|
|
93
|
+
consoleWarn: warnCalls.map((call) => call[0]),
|
|
94
|
+
}).toMatchInlineSnapshot(`
|
|
95
|
+
{
|
|
96
|
+
"consoleWarn": [
|
|
97
|
+
"Error detecting domain context, defaulting to cross-domain:",
|
|
98
|
+
],
|
|
99
|
+
"isCrossDomain": true,
|
|
100
|
+
}
|
|
101
|
+
`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("requires popup redirect uri for popup login", async () => {
|
|
105
|
+
const client = new AuthServerOAuth2Client(createConfig());
|
|
106
|
+
(client.config as { popupRedirectUri?: string }).popupRedirectUri =
|
|
107
|
+
undefined;
|
|
108
|
+
|
|
109
|
+
let error: Error | null = null;
|
|
110
|
+
try {
|
|
111
|
+
await client.loginWithPopup();
|
|
112
|
+
} catch (caught) {
|
|
113
|
+
error = caught as Error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expect({ error: error?.message }).toMatchInlineSnapshot(`
|
|
117
|
+
{
|
|
118
|
+
"error": "redirectUri is required for popup login. Either provide it as a parameter or configure popupRedirectUri in the client config.",
|
|
119
|
+
}
|
|
120
|
+
`);
|
|
121
|
+
});
|
|
122
|
+
});
|