@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.
@@ -1,4 +1,9 @@
1
- import {guardianValidation, AuthenticationStatus, User} from '../src/api';
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("return invalid cookie if missing", () => {
20
- expect(verifyUser(undefined, "", new Date(0), guardianValidation).status).toBe(AuthenticationStatus.INVALID_COOKIE);
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("return invalid cookie for a malformed signature", () => {
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
- expect(verifyUser(testCookie, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.INVALID_COOKIE);
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("return expired", () => {
31
- const someTimeInTheFuture = new Date(5678);
32
- expect(someTimeInTheFuture.getTime()).toBe(5678);
33
- expect(verifyUser(sampleCookie, publicKey, someTimeInTheFuture, guardianValidation).status).toBe(AuthenticationStatus.EXPIRED);
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("return not authenticated if user fails validation function", () => {
37
- expect(verifyUser(sampleCookieWithoutMultifactor, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.NOT_AUTHORISED);
38
- expect(verifyUser(sampleNonGuardianCookie, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.NOT_AUTHORISED);
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("return authenticated", () => {
42
- expect(verifyUser(sampleCookie, publicKey, new Date(0), guardianValidation).status).toBe(AuthenticationStatus.AUTHORISED);
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 return authenticated if valid', async () => {
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 { status } = await panda.verify(`cookiename=${sampleCookie}`);
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
- expect(status).toBe(AuthenticationStatus.AUTHORISED);
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 return expired if expired', async () => {
134
- jest.setSystemTime(10_000);
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 { status } = await panda.verify(`cookiename=${sampleCookie}`);
259
+ const authenticationResult = await panda.verify(`cookiename=${sampleCookie}`);
138
260
 
139
- expect(status).toBe(AuthenticationStatus.EXPIRED);
261
+ const expected: CookieFailure = {
262
+ success: false,
263
+ reason: 'expired-cookie'
264
+ };
265
+ expect(authenticationResult).toStrictEqual(expected);
140
266
  });
141
267
 
142
- it('should return not authenticated if validation fails', async () => {
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 { status } = await panda.verify(`cookiename=${sampleNonGuardianCookie}`);
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
- expect(status).toBe(AuthenticationStatus.NOT_AUTHORISED);
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
  });
@@ -3,11 +3,17 @@ import { sampleCookie } from './fixtures';
3
3
  import { URLSearchParams } from 'url';
4
4
 
5
5
  test("decode a cookie", () => {
6
- const { data, signature } = parseCookie(sampleCookie);
7
- expect(signature.length).toBe(684);
6
+ const parsedCookie = parseCookie(sampleCookie);
7
+ expect(parsedCookie).toBeDefined()
8
8
 
9
- const params = new URLSearchParams(data);
10
-
11
- expect(params.get("firstName")).toBe("Test");
12
- expect(params.get("lastName")).toBe("User");
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 }}