@guardian/pan-domain-node 0.5.1 → 1.0.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/CHANGELOG.md +112 -0
- package/CODEOWNERS +1 -1
- package/README.md +53 -13
- package/dist/src/api.d.ts +30 -9
- package/dist/src/api.js +13 -8
- package/dist/src/panda.d.ts +1 -1
- package/dist/src/panda.js +50 -12
- package/dist/src/utils.d.ts +6 -4
- package/dist/src/utils.js +23 -3
- package/dist/test/panda.test.js +196 -23
- package/dist/test/utils.test.js +10 -5
- package/package.json +1 -1
- package/src/api.ts +63 -10
- package/src/panda.ts +56 -19
- package/src/utils.ts +30 -4
- package/test/panda.test.ts +226 -25
- package/test/utils.test.ts +12 -6
- package/.github/workflows/snyk.yml +0 -19
package/test/panda.test.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CookieFailure, FreshSuccess,
|
|
3
|
+
gracePeriodInMillis,
|
|
4
|
+
guardianValidation, StaleSuccess,
|
|
5
|
+
User, UserValidationFailure
|
|
6
|
+
} from '../src/api';
|
|
2
7
|
import { verifyUser, createCookie, PanDomainAuthentication } from '../src/panda';
|
|
3
8
|
import { fetchPublicKey } from '../src/fetch-public-key';
|
|
4
9
|
|
|
@@ -9,37 +14,132 @@ import {
|
|
|
9
14
|
publicKey,
|
|
10
15
|
privateKey
|
|
11
16
|
} from './fixtures';
|
|
12
|
-
import {decodeBase64} from "../src/utils";
|
|
17
|
+
import {decodeBase64, parseCookie, ParsedCookie, parseUser} from "../src/utils";
|
|
13
18
|
|
|
14
19
|
jest.mock('../src/fetch-public-key');
|
|
15
20
|
jest.useFakeTimers('modern');
|
|
16
21
|
|
|
22
|
+
function userFromCookie(cookie: string): User {
|
|
23
|
+
// This function is only used to generate a `User` object from
|
|
24
|
+
// a well-formed text fixture cookie, in order to check that successful
|
|
25
|
+
// `AuthenticationResult`s have the right shape. As such we don't want
|
|
26
|
+
// to have to deal with the case of a bad cookie so we just cast to `ParsedCookie`.
|
|
27
|
+
const parsedCookie = parseCookie(cookie) as ParsedCookie;
|
|
28
|
+
return parseUser(parsedCookie.data);
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
describe('verifyUser', function () {
|
|
18
32
|
|
|
19
|
-
test("
|
|
20
|
-
|
|
33
|
+
test("fail to authenticate if cookie is missing", () => {
|
|
34
|
+
const expected: CookieFailure = {
|
|
35
|
+
success: false,
|
|
36
|
+
reason: 'no-cookie'
|
|
37
|
+
};
|
|
38
|
+
expect(verifyUser(undefined, "", new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
21
39
|
});
|
|
22
40
|
|
|
23
|
-
test("
|
|
41
|
+
test("fail to authenticate if signature is malformed", () => {
|
|
24
42
|
const [data, signature] = sampleCookie.split(".");
|
|
25
43
|
const testCookie = data + ".1234";
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
const expected: CookieFailure = {
|
|
46
|
+
success: false,
|
|
47
|
+
reason: 'invalid-cookie'
|
|
48
|
+
};
|
|
49
|
+
expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("fail to authenticate if cookie expired and we're outside the grace period", () => {
|
|
53
|
+
// Cookie expires at epoch time 1234
|
|
54
|
+
const afterEndOfGracePeriod = new Date(1234 + gracePeriodInMillis + 1)
|
|
55
|
+
const expected: CookieFailure = {
|
|
56
|
+
success: false,
|
|
57
|
+
reason: 'expired-cookie'
|
|
58
|
+
};
|
|
59
|
+
expect(verifyUser(sampleCookie, publicKey, afterEndOfGracePeriod, guardianValidation)).toStrictEqual(expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("fail to authenticate if user fails validation function", () => {
|
|
63
|
+
expect(verifyUser(sampleCookieWithoutMultifactor, publicKey, new Date(0), guardianValidation)).toStrictEqual({
|
|
64
|
+
success: false,
|
|
65
|
+
reason: 'invalid-user',
|
|
66
|
+
user: userFromCookie(sampleCookieWithoutMultifactor)
|
|
67
|
+
});
|
|
68
|
+
expect(verifyUser(sampleNonGuardianCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual({
|
|
69
|
+
success: false,
|
|
70
|
+
reason: 'invalid-user',
|
|
71
|
+
user: userFromCookie(sampleNonGuardianCookie)
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("fail to authenticate with invalid-cookie reason if signature is not valid", () => {
|
|
76
|
+
const expected: CookieFailure = {
|
|
77
|
+
success: false,
|
|
78
|
+
reason: 'invalid-cookie'
|
|
79
|
+
};
|
|
80
|
+
const slightlyBadCookie = sampleCookie.slice(0, -2);
|
|
81
|
+
expect(verifyUser(slightlyBadCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("fail to authenticate with invalid-cookie reason if data part is not base64", () => {
|
|
85
|
+
const expected: CookieFailure = {
|
|
86
|
+
success: false,
|
|
87
|
+
reason: 'invalid-cookie'
|
|
88
|
+
};
|
|
89
|
+
const [_, signature] = sampleCookie.split(".");
|
|
90
|
+
const nonBase64Data = "not-base64-data";
|
|
91
|
+
const testCookie = `${nonBase64Data}.${signature}`;
|
|
92
|
+
expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
28
93
|
});
|
|
29
94
|
|
|
30
|
-
test("
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
95
|
+
test("fail to authenticate with invalid-cookie reason if signature part is not base64", () => {
|
|
96
|
+
const expected: CookieFailure = {
|
|
97
|
+
success: false,
|
|
98
|
+
reason: 'invalid-cookie'
|
|
99
|
+
};
|
|
100
|
+
const [data, _] = sampleCookie.split(".");
|
|
101
|
+
const nonBase64Signature = "not-base64-signature";
|
|
102
|
+
const testCookie = `${data}.${nonBase64Signature}`;
|
|
103
|
+
expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
34
104
|
});
|
|
35
105
|
|
|
36
|
-
test("
|
|
37
|
-
|
|
38
|
-
|
|
106
|
+
test("fail to authenticate with invalid-cookie reason if cookie has no dot separator", () => {
|
|
107
|
+
const expected: CookieFailure = {
|
|
108
|
+
success: false,
|
|
109
|
+
reason: 'invalid-cookie'
|
|
110
|
+
};
|
|
111
|
+
const noDotCookie = sampleCookie.replace(".", "");
|
|
112
|
+
expect(verifyUser(noDotCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
39
113
|
});
|
|
40
114
|
|
|
41
|
-
test("
|
|
42
|
-
|
|
115
|
+
test("fail to authenticate with invalid-cookie reason if cookie has multiple dot separators", () => {
|
|
116
|
+
const expected: CookieFailure = {
|
|
117
|
+
success: false,
|
|
118
|
+
reason: 'invalid-cookie'
|
|
119
|
+
};
|
|
120
|
+
const multipleDotsCookie = sampleCookie.replace(".", "..");
|
|
121
|
+
expect(verifyUser(multipleDotsCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("authenticate if the cookie and user are valid", () => {
|
|
125
|
+
const expected: FreshSuccess = {
|
|
126
|
+
success: true,
|
|
127
|
+
// Cookie is not expired so no need to refresh credentials
|
|
128
|
+
shouldRefreshCredentials: false,
|
|
129
|
+
user: userFromCookie(sampleCookie)
|
|
130
|
+
};
|
|
131
|
+
expect(verifyUser(sampleCookie, publicKey, new Date(0), guardianValidation)).toStrictEqual(expected);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("authenticate with shouldRefreshCredentials if cookie expired but we're within the grace period", () => {
|
|
135
|
+
const beforeEndOfGracePeriod = new Date(1234 + gracePeriodInMillis - 1);
|
|
136
|
+
const expected: StaleSuccess = {
|
|
137
|
+
success: true,
|
|
138
|
+
user: userFromCookie(sampleCookie),
|
|
139
|
+
shouldRefreshCredentials: true,
|
|
140
|
+
mustRefreshByEpochTimeMillis: 1234 + gracePeriodInMillis
|
|
141
|
+
};
|
|
142
|
+
expect(verifyUser(sampleCookie, publicKey, beforeEndOfGracePeriod, guardianValidation)).toStrictEqual(expected);
|
|
43
143
|
});
|
|
44
144
|
});
|
|
45
145
|
|
|
@@ -122,32 +222,133 @@ describe('panda class', function () {
|
|
|
122
222
|
(fetchPublicKey as jest.MockedFunction<typeof fetchPublicKey>).mockResolvedValue({ key: publicKey, lastUpdated: new Date() });
|
|
123
223
|
});
|
|
124
224
|
|
|
125
|
-
it('should
|
|
225
|
+
it('should authenticate if cookie and user are valid', async () => {
|
|
126
226
|
jest.setSystemTime(100);
|
|
127
227
|
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
|
|
128
|
-
const
|
|
228
|
+
const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
|
|
229
|
+
|
|
230
|
+
const expected: FreshSuccess = {
|
|
231
|
+
success: true,
|
|
232
|
+
// Cookie is not expired
|
|
233
|
+
shouldRefreshCredentials: false,
|
|
234
|
+
user: userFromCookie(sampleCookie)
|
|
235
|
+
}
|
|
236
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
237
|
+
});
|
|
129
238
|
|
|
130
|
-
|
|
239
|
+
it('should authenticate if cookie and user are valid when multiple cookies are passed', async () => {
|
|
240
|
+
jest.setSystemTime(100);
|
|
241
|
+
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
|
|
242
|
+
const authenticationResult = await panda.verify(`a=blah; b=stuff; cookiename=${sampleCookie}; c=4958345`);
|
|
243
|
+
|
|
244
|
+
const expected: FreshSuccess = {
|
|
245
|
+
success: true,
|
|
246
|
+
// Cookie is not expired
|
|
247
|
+
shouldRefreshCredentials: false,
|
|
248
|
+
user: userFromCookie(sampleCookie)
|
|
249
|
+
};
|
|
250
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
131
251
|
});
|
|
132
252
|
|
|
133
|
-
it('should
|
|
134
|
-
|
|
253
|
+
it('should fail to authenticate if cookie expired and we\'re outside the grace period', async () => {
|
|
254
|
+
// Cookie expiry is 1234
|
|
255
|
+
const afterEndOfGracePeriodEpochMillis = 1234 + gracePeriodInMillis + 1
|
|
256
|
+
jest.setSystemTime(afterEndOfGracePeriodEpochMillis);
|
|
135
257
|
|
|
136
258
|
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
|
|
137
|
-
const
|
|
259
|
+
const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
|
|
138
260
|
|
|
139
|
-
|
|
261
|
+
const expected: CookieFailure = {
|
|
262
|
+
success: false,
|
|
263
|
+
reason: 'expired-cookie'
|
|
264
|
+
};
|
|
265
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
140
266
|
});
|
|
141
267
|
|
|
142
|
-
it('
|
|
268
|
+
it('authenticate with shouldRefreshCredentials if cookie expired but we\'re within the grace period', async () => {
|
|
269
|
+
// Cookie expiry is 1234
|
|
270
|
+
const beforeEndOfGracePeriodEpochMillis = 1234 + gracePeriodInMillis - 1;
|
|
271
|
+
jest.setSystemTime(beforeEndOfGracePeriodEpochMillis);
|
|
272
|
+
|
|
273
|
+
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u)=> true);
|
|
274
|
+
const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
|
|
275
|
+
|
|
276
|
+
const expected: StaleSuccess = {
|
|
277
|
+
success: true,
|
|
278
|
+
shouldRefreshCredentials: true,
|
|
279
|
+
mustRefreshByEpochTimeMillis: 1234 + gracePeriodInMillis,
|
|
280
|
+
user: userFromCookie(sampleCookie)
|
|
281
|
+
};
|
|
282
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should fail to authenticate if user is not valid', async () => {
|
|
143
286
|
jest.setSystemTime(100);
|
|
144
287
|
|
|
145
288
|
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
|
|
146
|
-
const
|
|
289
|
+
const authenticationResult = await panda.verify(`cookiename=${sampleNonGuardianCookie}`);
|
|
290
|
+
|
|
291
|
+
const expected: UserValidationFailure = {
|
|
292
|
+
success: false,
|
|
293
|
+
reason: 'invalid-user',
|
|
294
|
+
user: userFromCookie(sampleNonGuardianCookie)
|
|
295
|
+
};
|
|
296
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
297
|
+
});
|
|
147
298
|
|
|
148
|
-
|
|
299
|
+
it('should fail to authenticate if there is no cookie with the correct name', async () => {
|
|
300
|
+
jest.setSystemTime(100);
|
|
301
|
+
|
|
302
|
+
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
|
|
303
|
+
const authenticationResult = await panda.verify(`wrongcookiename=${sampleNonGuardianCookie}`);
|
|
304
|
+
|
|
305
|
+
const expected: CookieFailure = {
|
|
306
|
+
success: false,
|
|
307
|
+
reason: "no-cookie"
|
|
308
|
+
};
|
|
309
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
149
310
|
});
|
|
150
311
|
|
|
312
|
+
it('should fail to authenticate if the cookie request header is malformed', async () => {
|
|
313
|
+
jest.setSystemTime(100);
|
|
314
|
+
|
|
315
|
+
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
|
|
316
|
+
// The cookie headers should be semicolon-separated name=valueg
|
|
317
|
+
const authenticationResult = await panda.verify(sampleNonGuardianCookie);
|
|
318
|
+
|
|
319
|
+
const expected: CookieFailure = {
|
|
320
|
+
success: false,
|
|
321
|
+
reason: "no-cookie"
|
|
322
|
+
};
|
|
323
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should fail to authenticate if there is no cookie with the correct name out of multiple cookies', async () => {
|
|
327
|
+
jest.setSystemTime(100);
|
|
328
|
+
|
|
329
|
+
const panda = new PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', guardianValidation);
|
|
330
|
+
const authenticationResult = await panda.verify(`wrongcookiename=${sampleNonGuardianCookie}; anotherwrongcookiename=${sampleNonGuardianCookie}`);
|
|
331
|
+
|
|
332
|
+
const expected: CookieFailure = {
|
|
333
|
+
success: false,
|
|
334
|
+
reason: "no-cookie"
|
|
335
|
+
};
|
|
336
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should fail to authenticate with invalid-cookie reason if cookie is malformed', async () => {
|
|
340
|
+
jest.setSystemTime(100);
|
|
341
|
+
|
|
342
|
+
const panda = new PanDomainAuthentication('rightcookiename', 'region', 'bucket', 'keyfile', guardianValidation);
|
|
343
|
+
// There is a valid Panda cookie in here, but it's under the wrong name
|
|
344
|
+
const authenticationResult = await panda.verify(`wrongcookiename=${sampleNonGuardianCookie}; rightcookiename=not-valid-panda-cookie`);
|
|
345
|
+
|
|
346
|
+
const expected: CookieFailure = {
|
|
347
|
+
success: false,
|
|
348
|
+
reason: "invalid-cookie"
|
|
349
|
+
};
|
|
350
|
+
expect(authenticationResult).toStrictEqual(expected);
|
|
351
|
+
});
|
|
151
352
|
});
|
|
152
353
|
|
|
153
354
|
});
|
package/test/utils.test.ts
CHANGED
|
@@ -3,11 +3,17 @@ import { sampleCookie } from './fixtures';
|
|
|
3
3
|
import { URLSearchParams } from 'url';
|
|
4
4
|
|
|
5
5
|
test("decode a cookie", () => {
|
|
6
|
-
const
|
|
7
|
-
expect(
|
|
6
|
+
const parsedCookie = parseCookie(sampleCookie);
|
|
7
|
+
expect(parsedCookie).toBeDefined()
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// Unfortunately the above expect() doesn't narrow the type
|
|
10
|
+
if (parsedCookie) {
|
|
11
|
+
const { data, signature } = parsedCookie;
|
|
12
|
+
expect(signature.length).toBe(684);
|
|
13
|
+
|
|
14
|
+
const params = new URLSearchParams(data);
|
|
15
|
+
|
|
16
|
+
expect(params.get("firstName")).toBe("Test");
|
|
17
|
+
expect(params.get("lastName")).toBe("User");
|
|
18
|
+
}
|
|
13
19
|
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: Snyk
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
security:
|
|
11
|
-
uses: guardian/.github/.github/workflows/sbt-node-snyk.yml@main
|
|
12
|
-
with:
|
|
13
|
-
DEBUG: true
|
|
14
|
-
ORG: guardian
|
|
15
|
-
SKIP_NODE: false
|
|
16
|
-
NODE_VERSION_FILE: .nvmrc
|
|
17
|
-
|
|
18
|
-
secrets:
|
|
19
|
-
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|