@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.
@@ -16,26 +16,114 @@ const fixtures_1 = require("./fixtures");
16
16
  const utils_1 = require("../src/utils");
17
17
  jest.mock('../src/fetch-public-key');
18
18
  jest.useFakeTimers('modern');
19
+ function userFromCookie(cookie) {
20
+ // This function is only used to generate a `User` object from
21
+ // a well-formed text fixture cookie, in order to check that successful
22
+ // `AuthenticationResult`s have the right shape. As such we don't want
23
+ // to have to deal with the case of a bad cookie so we just cast to `ParsedCookie`.
24
+ const parsedCookie = utils_1.parseCookie(cookie);
25
+ return utils_1.parseUser(parsedCookie.data);
26
+ }
19
27
  describe('verifyUser', function () {
20
- test("return invalid cookie if missing", () => {
21
- expect(panda_1.verifyUser(undefined, "", new Date(0), api_1.guardianValidation).status).toBe(api_1.AuthenticationStatus.INVALID_COOKIE);
28
+ test("fail to authenticate if cookie is missing", () => {
29
+ const expected = {
30
+ success: false,
31
+ reason: 'no-cookie'
32
+ };
33
+ expect(panda_1.verifyUser(undefined, "", new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
22
34
  });
23
- test("return invalid cookie for a malformed signature", () => {
35
+ test("fail to authenticate if signature is malformed", () => {
24
36
  const [data, signature] = fixtures_1.sampleCookie.split(".");
25
37
  const testCookie = data + ".1234";
26
- expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation).status).toBe(api_1.AuthenticationStatus.INVALID_COOKIE);
38
+ const expected = {
39
+ success: false,
40
+ reason: 'invalid-cookie'
41
+ };
42
+ expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
27
43
  });
28
- test("return expired", () => {
29
- const someTimeInTheFuture = new Date(5678);
30
- expect(someTimeInTheFuture.getTime()).toBe(5678);
31
- expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, someTimeInTheFuture, api_1.guardianValidation).status).toBe(api_1.AuthenticationStatus.EXPIRED);
44
+ test("fail to authenticate if cookie expired and we're outside the grace period", () => {
45
+ // Cookie expires at epoch time 1234
46
+ const afterEndOfGracePeriod = new Date(1234 + api_1.gracePeriodInMillis + 1);
47
+ const expected = {
48
+ success: false,
49
+ reason: 'expired-cookie'
50
+ };
51
+ expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, afterEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected);
32
52
  });
33
- test("return not authenticated if user fails validation function", () => {
34
- expect(panda_1.verifyUser(fixtures_1.sampleCookieWithoutMultifactor, fixtures_1.publicKey, new Date(0), api_1.guardianValidation).status).toBe(api_1.AuthenticationStatus.NOT_AUTHORISED);
35
- expect(panda_1.verifyUser(fixtures_1.sampleNonGuardianCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation).status).toBe(api_1.AuthenticationStatus.NOT_AUTHORISED);
53
+ test("fail to authenticate if user fails validation function", () => {
54
+ expect(panda_1.verifyUser(fixtures_1.sampleCookieWithoutMultifactor, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({
55
+ success: false,
56
+ reason: 'invalid-user',
57
+ user: userFromCookie(fixtures_1.sampleCookieWithoutMultifactor)
58
+ });
59
+ expect(panda_1.verifyUser(fixtures_1.sampleNonGuardianCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({
60
+ success: false,
61
+ reason: 'invalid-user',
62
+ user: userFromCookie(fixtures_1.sampleNonGuardianCookie)
63
+ });
64
+ });
65
+ test("fail to authenticate with invalid-cookie reason if signature is not valid", () => {
66
+ const expected = {
67
+ success: false,
68
+ reason: 'invalid-cookie'
69
+ };
70
+ const slightlyBadCookie = fixtures_1.sampleCookie.slice(0, -2);
71
+ expect(panda_1.verifyUser(slightlyBadCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
72
+ });
73
+ test("fail to authenticate with invalid-cookie reason if data part is not base64", () => {
74
+ const expected = {
75
+ success: false,
76
+ reason: 'invalid-cookie'
77
+ };
78
+ const [_, signature] = fixtures_1.sampleCookie.split(".");
79
+ const nonBase64Data = "not-base64-data";
80
+ const testCookie = `${nonBase64Data}.${signature}`;
81
+ expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
82
+ });
83
+ test("fail to authenticate with invalid-cookie reason if signature part is not base64", () => {
84
+ const expected = {
85
+ success: false,
86
+ reason: 'invalid-cookie'
87
+ };
88
+ const [data, _] = fixtures_1.sampleCookie.split(".");
89
+ const nonBase64Signature = "not-base64-signature";
90
+ const testCookie = `${data}.${nonBase64Signature}`;
91
+ expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
92
+ });
93
+ test("fail to authenticate with invalid-cookie reason if cookie has no dot separator", () => {
94
+ const expected = {
95
+ success: false,
96
+ reason: 'invalid-cookie'
97
+ };
98
+ const noDotCookie = fixtures_1.sampleCookie.replace(".", "");
99
+ expect(panda_1.verifyUser(noDotCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
100
+ });
101
+ test("fail to authenticate with invalid-cookie reason if cookie has multiple dot separators", () => {
102
+ const expected = {
103
+ success: false,
104
+ reason: 'invalid-cookie'
105
+ };
106
+ const multipleDotsCookie = fixtures_1.sampleCookie.replace(".", "..");
107
+ expect(panda_1.verifyUser(multipleDotsCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
108
+ });
109
+ test("authenticate if the cookie and user are valid", () => {
110
+ const expected = {
111
+ success: true,
112
+ // Cookie is not expired so no need to refresh credentials
113
+ shouldRefreshCredentials: false,
114
+ user: userFromCookie(fixtures_1.sampleCookie)
115
+ };
116
+ expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
36
117
  });
37
- test("return authenticated", () => {
38
- expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation).status).toBe(api_1.AuthenticationStatus.AUTHORISED);
118
+ test("authenticate with shouldRefreshCredentials if cookie expired but we're within the grace period", () => {
119
+ const beforeEndOfGracePeriod = new Date(1234 + api_1.gracePeriodInMillis - 1);
120
+ const expected = {
121
+ success: true,
122
+ user: userFromCookie(fixtures_1.sampleCookie),
123
+ shouldRefreshCredentials: true,
124
+ mustRefreshByEpochTimeMillis: 1234 + api_1.gracePeriodInMillis
125
+ };
126
+ expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, beforeEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected);
39
127
  });
40
128
  });
41
129
  describe('createCookie', function () {
@@ -92,23 +180,108 @@ describe('panda class', function () {
92
180
  beforeEach(() => {
93
181
  fetch_public_key_1.fetchPublicKey.mockResolvedValue({ key: fixtures_1.publicKey, lastUpdated: new Date() });
94
182
  });
95
- it('should return authenticated if valid', () => __awaiter(this, void 0, void 0, function* () {
183
+ it('should authenticate if cookie and user are valid', () => __awaiter(this, void 0, void 0, function* () {
96
184
  jest.setSystemTime(100);
97
185
  const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true);
98
- const { status } = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`);
99
- expect(status).toBe(api_1.AuthenticationStatus.AUTHORISED);
186
+ const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`);
187
+ const expected = {
188
+ success: true,
189
+ // Cookie is not expired
190
+ shouldRefreshCredentials: false,
191
+ user: userFromCookie(fixtures_1.sampleCookie)
192
+ };
193
+ expect(authenticationResult).toStrictEqual(expected);
100
194
  }));
101
- it('should return expired if expired', () => __awaiter(this, void 0, void 0, function* () {
102
- jest.setSystemTime(10000);
195
+ it('should authenticate if cookie and user are valid when multiple cookies are passed', () => __awaiter(this, void 0, void 0, function* () {
196
+ jest.setSystemTime(100);
103
197
  const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true);
104
- const { status } = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`);
105
- expect(status).toBe(api_1.AuthenticationStatus.EXPIRED);
198
+ const authenticationResult = yield panda.verify(`a=blah; b=stuff; cookiename=${fixtures_1.sampleCookie}; c=4958345`);
199
+ const expected = {
200
+ success: true,
201
+ // Cookie is not expired
202
+ shouldRefreshCredentials: false,
203
+ user: userFromCookie(fixtures_1.sampleCookie)
204
+ };
205
+ expect(authenticationResult).toStrictEqual(expected);
206
+ }));
207
+ it('should fail to authenticate if cookie expired and we\'re outside the grace period', () => __awaiter(this, void 0, void 0, function* () {
208
+ // Cookie expiry is 1234
209
+ const afterEndOfGracePeriodEpochMillis = 1234 + api_1.gracePeriodInMillis + 1;
210
+ jest.setSystemTime(afterEndOfGracePeriodEpochMillis);
211
+ const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true);
212
+ const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`);
213
+ const expected = {
214
+ success: false,
215
+ reason: 'expired-cookie'
216
+ };
217
+ expect(authenticationResult).toStrictEqual(expected);
218
+ }));
219
+ it('authenticate with shouldRefreshCredentials if cookie expired but we\'re within the grace period', () => __awaiter(this, void 0, void 0, function* () {
220
+ // Cookie expiry is 1234
221
+ const beforeEndOfGracePeriodEpochMillis = 1234 + api_1.gracePeriodInMillis - 1;
222
+ jest.setSystemTime(beforeEndOfGracePeriodEpochMillis);
223
+ const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', (u) => true);
224
+ const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleCookie}`);
225
+ const expected = {
226
+ success: true,
227
+ shouldRefreshCredentials: true,
228
+ mustRefreshByEpochTimeMillis: 1234 + api_1.gracePeriodInMillis,
229
+ user: userFromCookie(fixtures_1.sampleCookie)
230
+ };
231
+ expect(authenticationResult).toStrictEqual(expected);
232
+ }));
233
+ it('should fail to authenticate if user is not valid', () => __awaiter(this, void 0, void 0, function* () {
234
+ jest.setSystemTime(100);
235
+ const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation);
236
+ const authenticationResult = yield panda.verify(`cookiename=${fixtures_1.sampleNonGuardianCookie}`);
237
+ const expected = {
238
+ success: false,
239
+ reason: 'invalid-user',
240
+ user: userFromCookie(fixtures_1.sampleNonGuardianCookie)
241
+ };
242
+ expect(authenticationResult).toStrictEqual(expected);
106
243
  }));
107
- it('should return not authenticated if validation fails', () => __awaiter(this, void 0, void 0, function* () {
244
+ it('should fail to authenticate if there is no cookie with the correct name', () => __awaiter(this, void 0, void 0, function* () {
108
245
  jest.setSystemTime(100);
109
246
  const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation);
110
- const { status } = yield panda.verify(`cookiename=${fixtures_1.sampleNonGuardianCookie}`);
111
- expect(status).toBe(api_1.AuthenticationStatus.NOT_AUTHORISED);
247
+ const authenticationResult = yield panda.verify(`wrongcookiename=${fixtures_1.sampleNonGuardianCookie}`);
248
+ const expected = {
249
+ success: false,
250
+ reason: "no-cookie"
251
+ };
252
+ expect(authenticationResult).toStrictEqual(expected);
253
+ }));
254
+ it('should fail to authenticate if the cookie request header is malformed', () => __awaiter(this, void 0, void 0, function* () {
255
+ jest.setSystemTime(100);
256
+ const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation);
257
+ // The cookie headers should be semicolon-separated name=valueg
258
+ const authenticationResult = yield panda.verify(fixtures_1.sampleNonGuardianCookie);
259
+ const expected = {
260
+ success: false,
261
+ reason: "no-cookie"
262
+ };
263
+ expect(authenticationResult).toStrictEqual(expected);
264
+ }));
265
+ it('should fail to authenticate if there is no cookie with the correct name out of multiple cookies', () => __awaiter(this, void 0, void 0, function* () {
266
+ jest.setSystemTime(100);
267
+ const panda = new panda_1.PanDomainAuthentication('cookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation);
268
+ const authenticationResult = yield panda.verify(`wrongcookiename=${fixtures_1.sampleNonGuardianCookie}; anotherwrongcookiename=${fixtures_1.sampleNonGuardianCookie}`);
269
+ const expected = {
270
+ success: false,
271
+ reason: "no-cookie"
272
+ };
273
+ expect(authenticationResult).toStrictEqual(expected);
274
+ }));
275
+ it('should fail to authenticate with invalid-cookie reason if cookie is malformed', () => __awaiter(this, void 0, void 0, function* () {
276
+ jest.setSystemTime(100);
277
+ const panda = new panda_1.PanDomainAuthentication('rightcookiename', 'region', 'bucket', 'keyfile', api_1.guardianValidation);
278
+ // There is a valid Panda cookie in here, but it's under the wrong name
279
+ const authenticationResult = yield panda.verify(`wrongcookiename=${fixtures_1.sampleNonGuardianCookie}; rightcookiename=not-valid-panda-cookie`);
280
+ const expected = {
281
+ success: false,
282
+ reason: "invalid-cookie"
283
+ };
284
+ expect(authenticationResult).toStrictEqual(expected);
112
285
  }));
113
286
  });
114
287
  });
@@ -4,9 +4,14 @@ const utils_1 = require("../src/utils");
4
4
  const fixtures_1 = require("./fixtures");
5
5
  const url_1 = require("url");
6
6
  test("decode a cookie", () => {
7
- const { data, signature } = utils_1.parseCookie(fixtures_1.sampleCookie);
8
- expect(signature.length).toBe(684);
9
- const params = new url_1.URLSearchParams(data);
10
- expect(params.get("firstName")).toBe("Test");
11
- expect(params.get("lastName")).toBe("User");
7
+ const parsedCookie = utils_1.parseCookie(fixtures_1.sampleCookie);
8
+ expect(parsedCookie).toBeDefined();
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
+ const params = new url_1.URLSearchParams(data);
14
+ expect(params.get("firstName")).toBe("Test");
15
+ expect(params.get("lastName")).toBe("User");
16
+ }
12
17
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guardian/pan-domain-node",
3
- "version": "0.5.1",
3
+ "version": "1.0.0",
4
4
  "description": "NodeJs implementation of Guardian pan-domain auth verification",
5
5
  "main": "dist/src/api.js",
6
6
  "types": "dist/src/api.d.ts",
package/src/api.ts CHANGED
@@ -1,11 +1,69 @@
1
1
  export { PanDomainAuthentication } from './panda';
2
2
 
3
- export enum AuthenticationStatus {
4
- INVALID_COOKIE = 'Invalid Cookie',
5
- EXPIRED = 'Expired',
6
- NOT_AUTHORISED = 'Not Authorised',
7
- AUTHORISED = 'Authorised'
3
+ // We continue to consider the request authenticated for
4
+ // a period of time after the cookie expiry. This is to allow
5
+ // API requests which cannot directly send the user for re-auth to
6
+ // indicate to the user that they must take some action to refresh their
7
+ // credentials (usually, refreshing the page).
8
+
9
+ // Panda cookie: issued expires
10
+ // | |
11
+ // |--1 hour--|
12
+ // Grace period: [------------- 24 hours ------]
13
+ // `success`: --false-][-true-----------------------------------][-false-------->
14
+ // `shouldRefreshCredentials` [-false---][-true------------------------]
15
+ export const gracePeriodInMillis = 24 * 60 * 60 * 1000;
16
+
17
+ // These are used to enforce the structure of the
18
+ // `AuthenticationResult` union types,
19
+ // but are not exported because they are too general.
20
+ interface Result {
21
+ success: boolean
22
+ }
23
+ interface Success extends Result {
24
+ // `success` is true when both these are true:
25
+ // 1. we've verified that the cookie is signed by the correct private key
26
+ // and decoded a `User` from it
27
+ // 2. we've validated the `User` using `ValidateUserFn`
28
+ success: true,
29
+ shouldRefreshCredentials: boolean,
30
+ user: User
31
+ }
32
+ interface Failure extends Result {
33
+ success: false,
34
+ reason: string
35
+ }
36
+
37
+ // These are members of the `AuthenticationResult` union,
38
+ // so they are exported for use by library consumers.
39
+ export interface FreshSuccess extends Success {
40
+ // Cookie has not expired yet, so no need to refresh credentials.
41
+ shouldRefreshCredentials: false
42
+ }
43
+ export interface StaleSuccess extends Success {
44
+ // Cookie has expired: we're in the grace period.
45
+ // Endpoints that can refresh credentials should do so,
46
+ // and those that cannot should tell the user to do so.
47
+ shouldRefreshCredentials: true,
48
+ mustRefreshByEpochTimeMillis: number
8
49
  }
50
+ export interface UserValidationFailure extends Failure {
51
+ reason: 'invalid-user',
52
+ user: User
53
+ }
54
+ export interface CookieFailure extends Failure {
55
+ reason: 'no-cookie' | 'invalid-cookie' | 'expired-cookie'
56
+ }
57
+ export interface UnknownFailure extends Failure {
58
+ reason: 'unknown'
59
+ }
60
+
61
+ export type AuthenticationResult = FreshSuccess
62
+ | StaleSuccess
63
+ | CookieFailure
64
+ | UserValidationFailure
65
+ | UnknownFailure
66
+
9
67
 
10
68
  export interface User {
11
69
  firstName: string,
@@ -18,11 +76,6 @@ export interface User {
18
76
  multifactor: boolean
19
77
  }
20
78
 
21
- export interface AuthenticationResult {
22
- status: AuthenticationStatus,
23
- user?: User
24
- }
25
-
26
79
  export type ValidateUserFn = (user: User) => boolean;
27
80
 
28
81
  export function guardianValidation(user: User): boolean {
package/src/panda.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as cookie from 'cookie';
2
2
 
3
3
  import {parseCookie, parseUser, sign, verifySignature} from './utils';
4
- import {AuthenticationStatus, User, AuthenticationResult, ValidateUserFn} from './api';
4
+ import {User, AuthenticationResult, ValidateUserFn, gracePeriodInMillis} from './api';
5
5
  import { fetchPublicKey, PublicKeyHolder } from './fetch-public-key';
6
6
 
7
7
  export function createCookie(user: User, privateKey: string): string {
@@ -25,34 +25,71 @@ export function createCookie(user: User, privateKey: string): string {
25
25
  }
26
26
 
27
27
  export function verifyUser(pandaCookie: string | undefined, publicKey: string, currentTime: Date, validateUser: ValidateUserFn): AuthenticationResult {
28
- if(!pandaCookie) {
29
- return { status: AuthenticationStatus.INVALID_COOKIE };
28
+ if (!pandaCookie) {
29
+ return {
30
+ success: false,
31
+ reason: 'no-cookie'
32
+ };
30
33
  }
31
34
 
32
- const { data, signature } = parseCookie(pandaCookie);
35
+ const parsedCookie = parseCookie(pandaCookie);
36
+ if (!parsedCookie) {
37
+ return {
38
+ success: false,
39
+ reason: 'invalid-cookie'
40
+ };
41
+ }
42
+ const { data, signature } = parsedCookie;
33
43
 
34
- if(!verifySignature(data, signature, publicKey)) {
35
- return { status: AuthenticationStatus.INVALID_COOKIE };
44
+ if (!verifySignature(data, signature, publicKey)) {
45
+ return {
46
+ success: false,
47
+ reason: 'invalid-cookie'
48
+ };
36
49
  }
37
50
 
38
- const currentTimestampInMilliseconds = currentTime.getTime();
51
+ const currentTimestampInMillis = currentTime.getTime();
39
52
 
40
53
  try {
41
54
  const user: User = parseUser(data);
42
- const isExpired = user.expires < currentTimestampInMilliseconds;
43
-
44
- if(isExpired) {
45
- return { status: AuthenticationStatus.EXPIRED, user };
55
+ const isExpired = user.expires < currentTimestampInMillis;
56
+
57
+ if (isExpired) {
58
+ const gracePeriodEndsAtEpochTimeMillis = user.expires + gracePeriodInMillis;
59
+ if (gracePeriodEndsAtEpochTimeMillis < currentTimestampInMillis) {
60
+ return {
61
+ success: false,
62
+ reason: 'expired-cookie'
63
+ };
64
+ } else {
65
+ return {
66
+ success: true,
67
+ shouldRefreshCredentials: true,
68
+ mustRefreshByEpochTimeMillis: gracePeriodEndsAtEpochTimeMillis,
69
+ user
70
+ }
71
+ }
46
72
  }
47
73
 
48
- if(!validateUser(user)) {
49
- return { status: AuthenticationStatus.NOT_AUTHORISED, user };
74
+ if (!validateUser(user)) {
75
+ return {
76
+ success: false,
77
+ reason: 'invalid-user',
78
+ user
79
+ };
50
80
  }
51
81
 
52
- return { status: AuthenticationStatus.AUTHORISED, user };
53
- } catch(error) {
82
+ return {
83
+ success: true,
84
+ shouldRefreshCredentials: false,
85
+ user
86
+ };
87
+ } catch (error) {
54
88
  console.error(error);
55
- return { status: AuthenticationStatus.INVALID_COOKIE };
89
+ return {
90
+ success: false,
91
+ reason: 'unknown'
92
+ };
56
93
  }
57
94
  }
58
95
 
@@ -64,7 +101,7 @@ export class PanDomainAuthentication {
64
101
  validateUser: ValidateUserFn;
65
102
 
66
103
  publicKey: Promise<PublicKeyHolder>;
67
- keyCacheTime: number = 60 * 1000; // 1 minute
104
+ keyCacheTimeInMillis: number = 60 * 1000; // 1 minute
68
105
  keyUpdateTimer?: NodeJS.Timeout;
69
106
 
70
107
  constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn) {
@@ -76,7 +113,7 @@ export class PanDomainAuthentication {
76
113
 
77
114
  this.publicKey = fetchPublicKey(region, bucket, keyFile);
78
115
 
79
- this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTime);
116
+ this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTimeInMillis);
80
117
  }
81
118
 
82
119
  stop(): void {
@@ -91,7 +128,7 @@ export class PanDomainAuthentication {
91
128
  const now = new Date();
92
129
  const diff = now.getTime() - lastUpdated.getTime();
93
130
 
94
- if(diff > this.keyCacheTime) {
131
+ if(diff > this.keyCacheTimeInMillis) {
95
132
  this.publicKey = fetchPublicKey(this.region, this.bucket, this.keyFile);
96
133
  return this.publicKey.then(({ key }) => key);
97
134
  } else {
package/src/utils.ts CHANGED
@@ -8,14 +8,40 @@ export function decodeBase64(data: string): string {
8
8
  return Buffer.from(data, 'base64').toString('utf8');
9
9
  }
10
10
 
11
+ export type ParsedCookie = { data: string, signature: string };
12
+
13
+ /**
14
+ * Check if a string is valid base64
15
+ */
16
+ function isBase64(str: string): boolean {
17
+ try {
18
+ return Buffer.from(str, 'base64').toString('base64') === str;
19
+ } catch (err) {
20
+ return false;
21
+ }
22
+ }
23
+
11
24
  /**
12
25
  * Parse a pan-domain user cookie in to data and signature
26
+ * Validates that the cookie is properly formatted (two base64 strings separated by '.')
13
27
  */
14
- export function parseCookie(cookie: string): { data: string, signature: string} {
15
- const splitCookie = cookie.split('\.');
28
+ export function parseCookie(cookie: string): ParsedCookie | undefined {
29
+ const cookieRegex = /^([\w\W]*)\.([\w\W]*)$/;
30
+ const match = cookie.match(cookieRegex);
31
+
32
+ if (!match) {
33
+ return undefined;
34
+ }
35
+
36
+ const [, data, signature] = match;
37
+
38
+ if (!isBase64(data) || !isBase64(signature)) {
39
+ return undefined;
40
+ }
41
+
16
42
  return {
17
- data: decodeBase64(splitCookie[0]),
18
- signature: splitCookie[1]
43
+ data: decodeBase64(data),
44
+ signature: signature
19
45
  };
20
46
  }
21
47