@guardian/pan-domain-node 1.0.0 → 1.1.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @guardian/pan-domain-node
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b762261: Download public key from S3 with AWS credential
8
+
3
9
  ## 1.0.0
4
10
 
5
11
  ### Major Changes
package/README.md CHANGED
@@ -40,16 +40,27 @@ Grace period: [------------- 24 hours ------]
40
40
  npm install --save-dev @guardian/pan-domain-node
41
41
  ```
42
42
 
43
+ ### Setup
44
+ The library load the public key file from a S3 object. Consuming applications can specify the S3 object via the arugments 'region', 'bucket' and 'keyFile' to the constructor of `PanDomainAuthentication` class as shown in the Initialisation below.
45
+
46
+ Therefore, the application must run with an AWS credential that has read access to the S3 object in that bucket.
47
+
48
+ You may refer to [Pan Domain authentication documentation](https://github.com/guardian/pan-domain-authentication) for details on how this authentication works.
49
+
43
50
  ### Initialisation
44
51
  ```typescript
45
52
  import { PanDomainAuthentication, AuthenticationStatus, User, guardianValidation } from '@guardian/pan-domain-node';
53
+ import { fromIni } from "@aws-sdk/credential-providers";
54
+
55
+ const credentialsProvider = fromIni(); // get credentials locally using the default profile
46
56
 
47
57
  const panda = new PanDomainAuthentication(
48
58
  "gutoolsAuth-assym", // cookie name
49
59
  "eu-west-1", // AWS region
50
60
  "pan-domain-auth-settings", // Settings bucket
51
61
  "local.dev-gutools.co.uk.settings.public", // Settings file
52
- guardianValidation
62
+ guardianValidation,
63
+ credentialsProvider, // it can be omitted if the app runs in AWS cloud. In this case, "fromNodeProviderChain" is used by default.
53
64
  );
54
65
 
55
66
  // alternatively customise the validation function and pass at construction
package/dist/src/api.d.ts CHANGED
@@ -29,7 +29,7 @@ export interface CookieFailure extends Failure {
29
29
  export interface UnknownFailure extends Failure {
30
30
  reason: 'unknown';
31
31
  }
32
- export declare type AuthenticationResult = FreshSuccess | StaleSuccess | CookieFailure | UserValidationFailure | UnknownFailure;
32
+ export type AuthenticationResult = FreshSuccess | StaleSuccess | CookieFailure | UserValidationFailure | UnknownFailure;
33
33
  export interface User {
34
34
  firstName: string;
35
35
  lastName: string;
@@ -40,5 +40,5 @@ export interface User {
40
40
  expires: number;
41
41
  multifactor: boolean;
42
42
  }
43
- export declare type ValidateUserFn = (user: User) => boolean;
43
+ export type ValidateUserFn = (user: User) => boolean;
44
44
  export declare function guardianValidation(user: User): boolean;
package/dist/src/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.guardianValidation = exports.gracePeriodInMillis = void 0;
3
+ exports.gracePeriodInMillis = exports.PanDomainAuthentication = void 0;
4
+ exports.guardianValidation = guardianValidation;
4
5
  var panda_1 = require("./panda");
5
6
  Object.defineProperty(exports, "PanDomainAuthentication", { enumerable: true, get: function () { return panda_1.PanDomainAuthentication; } });
6
7
  // We continue to consider the request authenticated for
@@ -19,4 +20,3 @@ function guardianValidation(user) {
19
20
  const isGuardianUser = user.email.indexOf('guardian.co.uk') !== -1;
20
21
  return isGuardianUser && user.multifactor;
21
22
  }
22
- exports.guardianValidation = guardianValidation;
@@ -1,5 +1,6 @@
1
+ import { S3 } from "@aws-sdk/client-s3";
1
2
  export interface PublicKeyHolder {
2
3
  key: string;
3
4
  lastUpdated: Date;
4
5
  }
5
- export declare function fetchPublicKey(region: string, bucket: String, keyFile: String): Promise<PublicKeyHolder>;
6
+ export declare function fetchPublicKey(s3: S3, bucket: string, keyFile: string): Promise<PublicKeyHolder>;
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -11,30 +15,54 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
11
15
  }) : function(o, v) {
12
16
  o["default"] = v;
13
17
  });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
20
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
21
35
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.fetchPublicKey = void 0;
36
+ exports.fetchPublicKey = fetchPublicKey;
23
37
  const iniparser = __importStar(require("iniparser"));
24
38
  const utils_1 = require("./utils");
25
- function fetchPublicKey(region, bucket, keyFile) {
26
- const path = `https://s3.${region}.amazonaws.com/${bucket}/${keyFile}`;
27
- return utils_1.httpGet(path).then(response => {
28
- const config = iniparser.parseString(response);
29
- if (config.publicKey) {
30
- return {
31
- key: utils_1.base64ToPEM(config.publicKey, "PUBLIC"),
32
- lastUpdated: new Date()
33
- };
39
+ function fetchPublicKey(s3, bucket, keyFile) {
40
+ const publicKeyLocation = {
41
+ Bucket: bucket,
42
+ Key: keyFile,
43
+ };
44
+ return s3.getObject(publicKeyLocation)
45
+ .then(({ Body }) => Body === null || Body === void 0 ? void 0 : Body.transformToString())
46
+ .then((pandaConfigIni) => {
47
+ if (!pandaConfigIni) {
48
+ throw Error(`could not read panda config ${JSON.stringify(publicKeyLocation)}`);
34
49
  }
35
50
  else {
36
- throw new Error("Missing publicKey setting from config");
51
+ const config = iniparser.parseString(pandaConfigIni);
52
+ if (config.publicKey) {
53
+ return {
54
+ key: (0, utils_1.base64ToPEM)(config.publicKey, "PUBLIC"),
55
+ lastUpdated: new Date()
56
+ };
57
+ }
58
+ else {
59
+ console.log(`Failed to retrieve panda public key from ${JSON.stringify(config)}`);
60
+ throw new Error("Missing publicKey setting from config");
61
+ }
37
62
  }
63
+ })
64
+ .catch((error) => {
65
+ console.error(`Error fetching public key from S3: ${error}`);
66
+ throw error;
38
67
  });
39
68
  }
40
- exports.fetchPublicKey = fetchPublicKey;
@@ -1,6 +1,7 @@
1
- /// <reference types="node" />
2
1
  import { User, AuthenticationResult, ValidateUserFn } from './api';
3
2
  import { PublicKeyHolder } from './fetch-public-key';
3
+ import { S3 } from "@aws-sdk/client-s3";
4
+ import { AwsCredentialIdentityProvider } from "@aws-sdk/types";
4
5
  export declare function createCookie(user: User, privateKey: string): string;
5
6
  export declare function verifyUser(pandaCookie: string | undefined, publicKey: string, currentTime: Date, validateUser: ValidateUserFn): AuthenticationResult;
6
7
  export declare class PanDomainAuthentication {
@@ -12,7 +13,8 @@ export declare class PanDomainAuthentication {
12
13
  publicKey: Promise<PublicKeyHolder>;
13
14
  keyCacheTimeInMillis: number;
14
15
  keyUpdateTimer?: NodeJS.Timeout;
15
- constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn);
16
+ s3Client: S3;
17
+ constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn, credentialsProvider?: AwsCredentialIdentityProvider);
16
18
  stop(): void;
17
19
  getPublicKey(): Promise<string>;
18
20
  verify(requestCookies: string): Promise<AuthenticationResult>;
package/dist/src/panda.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -11,19 +15,33 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
11
15
  }) : function(o, v) {
12
16
  o["default"] = v;
13
17
  });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
20
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
21
35
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.PanDomainAuthentication = exports.verifyUser = exports.createCookie = void 0;
36
+ exports.PanDomainAuthentication = void 0;
37
+ exports.createCookie = createCookie;
38
+ exports.verifyUser = verifyUser;
23
39
  const cookie = __importStar(require("cookie"));
24
40
  const utils_1 = require("./utils");
25
41
  const api_1 = require("./api");
26
42
  const fetch_public_key_1 = require("./fetch-public-key");
43
+ const client_s3_1 = require("@aws-sdk/client-s3");
44
+ const credential_providers_1 = require("@aws-sdk/credential-providers");
27
45
  function createCookie(user, privateKey) {
28
46
  let queryParams = [];
29
47
  queryParams.push("firstName=" + user.firstName);
@@ -36,10 +54,9 @@ function createCookie(user, privateKey) {
36
54
  queryParams.push("multifactor=" + String(user.multifactor));
37
55
  const combined = queryParams.join("&");
38
56
  const queryParamsString = Buffer.from(combined).toString('base64');
39
- const signature = utils_1.sign(combined, privateKey);
57
+ const signature = (0, utils_1.sign)(combined, privateKey);
40
58
  return queryParamsString + "." + signature;
41
59
  }
42
- exports.createCookie = createCookie;
43
60
  function verifyUser(pandaCookie, publicKey, currentTime, validateUser) {
44
61
  if (!pandaCookie) {
45
62
  return {
@@ -47,7 +64,7 @@ function verifyUser(pandaCookie, publicKey, currentTime, validateUser) {
47
64
  reason: 'no-cookie'
48
65
  };
49
66
  }
50
- const parsedCookie = utils_1.parseCookie(pandaCookie);
67
+ const parsedCookie = (0, utils_1.parseCookie)(pandaCookie);
51
68
  if (!parsedCookie) {
52
69
  return {
53
70
  success: false,
@@ -55,7 +72,7 @@ function verifyUser(pandaCookie, publicKey, currentTime, validateUser) {
55
72
  };
56
73
  }
57
74
  const { data, signature } = parsedCookie;
58
- if (!utils_1.verifySignature(data, signature, publicKey)) {
75
+ if (!(0, utils_1.verifySignature)(data, signature, publicKey)) {
59
76
  return {
60
77
  success: false,
61
78
  reason: 'invalid-cookie'
@@ -63,7 +80,7 @@ function verifyUser(pandaCookie, publicKey, currentTime, validateUser) {
63
80
  }
64
81
  const currentTimestampInMillis = currentTime.getTime();
65
82
  try {
66
- const user = utils_1.parseUser(data);
83
+ const user = (0, utils_1.parseUser)(data);
67
84
  const isExpired = user.expires < currentTimestampInMillis;
68
85
  if (isExpired) {
69
86
  const gracePeriodEndsAtEpochTimeMillis = user.expires + api_1.gracePeriodInMillis;
@@ -103,16 +120,20 @@ function verifyUser(pandaCookie, publicKey, currentTime, validateUser) {
103
120
  };
104
121
  }
105
122
  }
106
- exports.verifyUser = verifyUser;
107
123
  class PanDomainAuthentication {
108
- constructor(cookieName, region, bucket, keyFile, validateUser) {
124
+ constructor(cookieName, region, bucket, keyFile, validateUser, credentialsProvider = (0, credential_providers_1.fromNodeProviderChain)()) {
109
125
  this.keyCacheTimeInMillis = 60 * 1000; // 1 minute
110
126
  this.cookieName = cookieName;
111
127
  this.region = region;
112
128
  this.bucket = bucket;
113
129
  this.keyFile = keyFile;
114
130
  this.validateUser = validateUser;
115
- this.publicKey = fetch_public_key_1.fetchPublicKey(region, bucket, keyFile);
131
+ const standardAwsConfig = {
132
+ region: region,
133
+ credentials: credentialsProvider,
134
+ };
135
+ this.s3Client = new client_s3_1.S3(standardAwsConfig);
136
+ this.publicKey = (0, fetch_public_key_1.fetchPublicKey)(this.s3Client, bucket, keyFile);
116
137
  this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTimeInMillis);
117
138
  }
118
139
  stop() {
@@ -126,7 +147,7 @@ class PanDomainAuthentication {
126
147
  const now = new Date();
127
148
  const diff = now.getTime() - lastUpdated.getTime();
128
149
  if (diff > this.keyCacheTimeInMillis) {
129
- this.publicKey = fetch_public_key_1.fetchPublicKey(this.region, this.bucket, this.keyFile);
150
+ this.publicKey = (0, fetch_public_key_1.fetchPublicKey)(this.s3Client, this.bucket, this.keyFile);
130
151
  return this.publicKey.then(({ key }) => key);
131
152
  }
132
153
  else {
@@ -1,6 +1,6 @@
1
1
  import { User } from './api';
2
2
  export declare function decodeBase64(data: string): string;
3
- export declare type ParsedCookie = {
3
+ export type ParsedCookie = {
4
4
  data: string;
5
5
  signature: string;
6
6
  };
package/dist/src/utils.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -11,22 +15,37 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
11
15
  }) : function(o, v) {
12
16
  o["default"] = v;
13
17
  });
14
- var __importStar = (this && this.__importStar) || function (mod) {
15
- if (mod && mod.__esModule) return mod;
16
- var result = {};
17
- if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
- __setModuleDefault(result, mod);
19
- return result;
20
- };
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
21
35
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.parseUser = exports.httpGet = exports.base64ToPEM = exports.sign = exports.verifySignature = exports.parseCookie = exports.decodeBase64 = void 0;
36
+ exports.decodeBase64 = decodeBase64;
37
+ exports.parseCookie = parseCookie;
38
+ exports.verifySignature = verifySignature;
39
+ exports.sign = sign;
40
+ exports.base64ToPEM = base64ToPEM;
41
+ exports.httpGet = httpGet;
42
+ exports.parseUser = parseUser;
23
43
  const crypto = __importStar(require("crypto"));
24
44
  const https = __importStar(require("https"));
25
45
  const url_1 = require("url");
26
46
  function decodeBase64(data) {
27
47
  return Buffer.from(data, 'base64').toString('utf8');
28
48
  }
29
- exports.decodeBase64 = decodeBase64;
30
49
  /**
31
50
  * Check if a string is valid base64
32
51
  */
@@ -57,7 +76,6 @@ function parseCookie(cookie) {
57
76
  signature: signature
58
77
  };
59
78
  }
60
- exports.parseCookie = parseCookie;
61
79
  /**
62
80
  * Verify signed data using nodeJs crypto library
63
81
  */
@@ -66,14 +84,12 @@ function verifySignature(message, signature, pandaPublicKey) {
66
84
  .update(message, 'utf8')
67
85
  .verify(pandaPublicKey, signature, 'base64');
68
86
  }
69
- exports.verifySignature = verifySignature;
70
87
  function sign(message, privateKey) {
71
88
  const sign = crypto.createSign("sha256WithRSAEncryption");
72
89
  sign.write(message);
73
90
  sign.end();
74
91
  return sign.sign(privateKey, 'base64');
75
92
  }
76
- exports.sign = sign;
77
93
  const ASCII_NEW_LINE = String.fromCharCode(10);
78
94
  function base64ToPEM(key, headerFooter) {
79
95
  const PEM_HEADER = `-----BEGIN ${headerFooter} KEY-----`;
@@ -91,7 +107,6 @@ function base64ToPEM(key, headerFooter) {
91
107
  ret.push(Buffer.from(PEM_FOOTER).toString('ascii'));
92
108
  return ret.join(ASCII_NEW_LINE);
93
109
  }
94
- exports.base64ToPEM = base64ToPEM;
95
110
  function httpGet(path) {
96
111
  return new Promise((resolve, reject) => {
97
112
  const data = [];
@@ -113,7 +128,6 @@ function httpGet(path) {
113
128
  });
114
129
  });
115
130
  }
116
- exports.httpGet = httpGet;
117
131
  function parseUser(data) {
118
132
  const params = new url_1.URLSearchParams(data);
119
133
  function stringField(name) {
@@ -152,4 +166,3 @@ function parseUser(data) {
152
166
  multifactor: booleanField("multifactor")
153
167
  };
154
168
  }
155
- exports.parseUser = parseUser;
@@ -3,9 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sampleNonGuardianCookie = exports.sampleCookieWithoutMultifactor = exports.sampleCookie = exports.publicKey = exports.encodedPublicKey = exports.privateKey = exports.encodedPrivateKey = void 0;
4
4
  const utils_1 = require("../src/utils");
5
5
  exports.encodedPrivateKey = "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";
6
- exports.privateKey = utils_1.base64ToPEM(exports.encodedPrivateKey, "RSA PRIVATE");
6
+ exports.privateKey = (0, utils_1.base64ToPEM)(exports.encodedPrivateKey, "RSA PRIVATE");
7
7
  exports.encodedPublicKey = "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAur0hOjhB2QWjwOopCR+Qo27AYv97BJkVaKWPXpj9RfvY1wtpIratDN6tkXN9WCRPzVX8+5qaW034Kvf9WwBZD1ntS8iHYwY1YUaU8Mrp2sRT3K0RqyBlTswIH3HIqpASqv7ZtwDHdhk7Cbd13P5aomJSOjYFhCDUi3sRbjJP1kb6uQLdkZj8fIU518HzSR7Kw7p2mbDqSrGbnaeHWd0Tr3BvDHp9Pi0KpSAVm2qWAXix+BcjMA4ar7kLU1Pre0lt4K4DlSvq5XoHdX9/yvS6KGf+8pXDR9bY6dgRPSG4mzKpiKfkv1eXE8WKs0q3217QZItaSjocw4d0o47vSN+/MAh9V5Zewyb6ogs8JicX3Y3FPG29I1g8iLf3kBZ7V4mUimGuOq/L+1YVvyOTb2zWWMjNmECO2lrxXJc5LWRs5FmSJyCdilRktDE0WTGUo89O+DcF2752qtpUmlV2fllU1LXIAn0TJKiAZspKakamifrgYFIzZK4oZ8wDeFesQB/a/U7wtyv85vknzCtMLI28dnpGQ/ZFHNqWYVaHoHsnEmWion7lgMctnpY5pwKFfUSfZecl2Xqwjk1HZj71A9TFNQj+/x4z957cNtx+utAkGinK3eZF+H1o5YnSgjg4hN41kbXttk8nADerPdF7hDS6np7xzUl6qOicJhEOJ5x0c18CAwEAAQ==";
8
- exports.publicKey = utils_1.base64ToPEM(exports.encodedPublicKey, "PUBLIC");
8
+ exports.publicKey = (0, utils_1.base64ToPEM)(exports.encodedPublicKey, "PUBLIC");
9
9
  // The comments above each fixture are the Scala case classe representation used to generate the encoded cookie
10
10
  /*
11
11
  val user = AuthenticatedUser(
@@ -15,14 +15,14 @@ const fetch_public_key_1 = require("../src/fetch-public-key");
15
15
  const fixtures_1 = require("./fixtures");
16
16
  const utils_1 = require("../src/utils");
17
17
  jest.mock('../src/fetch-public-key');
18
- jest.useFakeTimers('modern');
18
+ jest.useFakeTimers();
19
19
  function userFromCookie(cookie) {
20
20
  // This function is only used to generate a `User` object from
21
21
  // a well-formed text fixture cookie, in order to check that successful
22
22
  // `AuthenticationResult`s have the right shape. As such we don't want
23
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);
24
+ const parsedCookie = (0, utils_1.parseCookie)(cookie);
25
+ return (0, utils_1.parseUser)(parsedCookie.data);
26
26
  }
27
27
  describe('verifyUser', function () {
28
28
  test("fail to authenticate if cookie is missing", () => {
@@ -30,7 +30,7 @@ describe('verifyUser', function () {
30
30
  success: false,
31
31
  reason: 'no-cookie'
32
32
  };
33
- expect(panda_1.verifyUser(undefined, "", new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
33
+ expect((0, panda_1.verifyUser)(undefined, "", new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
34
34
  });
35
35
  test("fail to authenticate if signature is malformed", () => {
36
36
  const [data, signature] = fixtures_1.sampleCookie.split(".");
@@ -39,7 +39,7 @@ describe('verifyUser', function () {
39
39
  success: false,
40
40
  reason: 'invalid-cookie'
41
41
  };
42
- expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
42
+ expect((0, panda_1.verifyUser)(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
43
43
  });
44
44
  test("fail to authenticate if cookie expired and we're outside the grace period", () => {
45
45
  // Cookie expires at epoch time 1234
@@ -48,15 +48,15 @@ describe('verifyUser', function () {
48
48
  success: false,
49
49
  reason: 'expired-cookie'
50
50
  };
51
- expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, afterEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected);
51
+ expect((0, panda_1.verifyUser)(fixtures_1.sampleCookie, fixtures_1.publicKey, afterEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected);
52
52
  });
53
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({
54
+ expect((0, panda_1.verifyUser)(fixtures_1.sampleCookieWithoutMultifactor, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({
55
55
  success: false,
56
56
  reason: 'invalid-user',
57
57
  user: userFromCookie(fixtures_1.sampleCookieWithoutMultifactor)
58
58
  });
59
- expect(panda_1.verifyUser(fixtures_1.sampleNonGuardianCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({
59
+ expect((0, panda_1.verifyUser)(fixtures_1.sampleNonGuardianCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual({
60
60
  success: false,
61
61
  reason: 'invalid-user',
62
62
  user: userFromCookie(fixtures_1.sampleNonGuardianCookie)
@@ -68,7 +68,7 @@ describe('verifyUser', function () {
68
68
  reason: 'invalid-cookie'
69
69
  };
70
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);
71
+ expect((0, panda_1.verifyUser)(slightlyBadCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
72
72
  });
73
73
  test("fail to authenticate with invalid-cookie reason if data part is not base64", () => {
74
74
  const expected = {
@@ -78,7 +78,7 @@ describe('verifyUser', function () {
78
78
  const [_, signature] = fixtures_1.sampleCookie.split(".");
79
79
  const nonBase64Data = "not-base64-data";
80
80
  const testCookie = `${nonBase64Data}.${signature}`;
81
- expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
81
+ expect((0, panda_1.verifyUser)(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
82
82
  });
83
83
  test("fail to authenticate with invalid-cookie reason if signature part is not base64", () => {
84
84
  const expected = {
@@ -88,7 +88,7 @@ describe('verifyUser', function () {
88
88
  const [data, _] = fixtures_1.sampleCookie.split(".");
89
89
  const nonBase64Signature = "not-base64-signature";
90
90
  const testCookie = `${data}.${nonBase64Signature}`;
91
- expect(panda_1.verifyUser(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
91
+ expect((0, panda_1.verifyUser)(testCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
92
92
  });
93
93
  test("fail to authenticate with invalid-cookie reason if cookie has no dot separator", () => {
94
94
  const expected = {
@@ -96,7 +96,7 @@ describe('verifyUser', function () {
96
96
  reason: 'invalid-cookie'
97
97
  };
98
98
  const noDotCookie = fixtures_1.sampleCookie.replace(".", "");
99
- expect(panda_1.verifyUser(noDotCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
99
+ expect((0, panda_1.verifyUser)(noDotCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
100
100
  });
101
101
  test("fail to authenticate with invalid-cookie reason if cookie has multiple dot separators", () => {
102
102
  const expected = {
@@ -104,7 +104,7 @@ describe('verifyUser', function () {
104
104
  reason: 'invalid-cookie'
105
105
  };
106
106
  const multipleDotsCookie = fixtures_1.sampleCookie.replace(".", "..");
107
- expect(panda_1.verifyUser(multipleDotsCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
107
+ expect((0, panda_1.verifyUser)(multipleDotsCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
108
108
  });
109
109
  test("authenticate if the cookie and user are valid", () => {
110
110
  const expected = {
@@ -113,7 +113,7 @@ describe('verifyUser', function () {
113
113
  shouldRefreshCredentials: false,
114
114
  user: userFromCookie(fixtures_1.sampleCookie)
115
115
  };
116
- expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
116
+ expect((0, panda_1.verifyUser)(fixtures_1.sampleCookie, fixtures_1.publicKey, new Date(0), api_1.guardianValidation)).toStrictEqual(expected);
117
117
  });
118
118
  test("authenticate with shouldRefreshCredentials if cookie expired but we're within the grace period", () => {
119
119
  const beforeEndOfGracePeriod = new Date(1234 + api_1.gracePeriodInMillis - 1);
@@ -123,7 +123,7 @@ describe('verifyUser', function () {
123
123
  shouldRefreshCredentials: true,
124
124
  mustRefreshByEpochTimeMillis: 1234 + api_1.gracePeriodInMillis
125
125
  };
126
- expect(panda_1.verifyUser(fixtures_1.sampleCookie, fixtures_1.publicKey, beforeEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected);
126
+ expect((0, panda_1.verifyUser)(fixtures_1.sampleCookie, fixtures_1.publicKey, beforeEndOfGracePeriod, api_1.guardianValidation)).toStrictEqual(expected);
127
127
  });
128
128
  });
129
129
  describe('createCookie', function () {
@@ -137,8 +137,8 @@ describe('createCookie', function () {
137
137
  expires: 1234,
138
138
  multifactor: true
139
139
  };
140
- const cookie = panda_1.createCookie(user, fixtures_1.privateKey);
141
- expect(utils_1.decodeBase64(cookie)).toEqual(utils_1.decodeBase64(fixtures_1.sampleCookie));
140
+ const cookie = (0, panda_1.createCookie)(user, fixtures_1.privateKey);
141
+ expect((0, utils_1.decodeBase64)(cookie)).toEqual((0, utils_1.decodeBase64)(fixtures_1.sampleCookie));
142
142
  expect(cookie).toEqual(fixtures_1.sampleCookie);
143
143
  });
144
144
  });
@@ -4,7 +4,7 @@ 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 parsedCookie = utils_1.parseCookie(fixtures_1.sampleCookie);
7
+ const parsedCookie = (0, utils_1.parseCookie)(fixtures_1.sampleCookie);
8
8
  expect(parsedCookie).toBeDefined();
9
9
  // Unfortunately the above expect() doesn't narrow the type
10
10
  if (parsedCookie) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guardian/pan-domain-node",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",
@@ -25,13 +25,16 @@
25
25
  "@changesets/cli": "^2.27.10",
26
26
  "@types/cookie": "^0.4.0",
27
27
  "@types/iniparser": "0.0.29",
28
- "@types/jest": "^26.0.9",
29
- "@types/node": "^14.0.27",
30
- "jest": "^26.2.2",
31
- "ts-jest": "^26.1.4",
32
- "typescript": "^3.9.7"
28
+ "@types/jest": "^29.5.14",
29
+ "@types/node": "^22.10.1",
30
+ "jest": "^29.7.0",
31
+ "ts-jest": "^29.2.5",
32
+ "typescript": "^5.7.3"
33
33
  },
34
34
  "dependencies": {
35
+ "@aws-sdk/client-s3": "^3.299.0",
36
+ "@aws-sdk/credential-providers": "^3.299.0",
37
+ "@aws-sdk/types": "^3.299.0",
35
38
  "cookie": "^0.4.1",
36
39
  "iniparser": "^1.0.5"
37
40
  },
@@ -1,5 +1,6 @@
1
1
  import * as iniparser from 'iniparser';
2
2
  import {base64ToPEM, httpGet} from './utils';
3
+ import { S3 } from "@aws-sdk/client-s3";
3
4
 
4
5
  export interface PublicKeyHolder {
5
6
  key: string,
@@ -7,21 +8,36 @@ export interface PublicKeyHolder {
7
8
  }
8
9
 
9
10
 
10
- export function fetchPublicKey(region: string, bucket: String, keyFile: String): Promise<PublicKeyHolder> {
11
- const path = `https://s3.${region}.amazonaws.com/${bucket}/${keyFile}`;
11
+ export function fetchPublicKey(s3: S3, bucket: string, keyFile: string): Promise<PublicKeyHolder> {
12
12
 
13
- return httpGet(path).then(response => {
14
- const config: { publicKey?: string} = iniparser.parseString(response);
13
+ const publicKeyLocation = {
14
+ Bucket: bucket,
15
+ Key: keyFile,
16
+ };
15
17
 
16
- if(config.publicKey) {
17
- return {
18
- key: base64ToPEM(config.publicKey, "PUBLIC"),
19
- lastUpdated: new Date()
20
- };
21
- } else {
22
- throw new Error("Missing publicKey setting from config");
23
- }
24
- });
18
+ return s3.getObject(publicKeyLocation)
19
+ .then(({ Body }) => Body?.transformToString())
20
+ .then((pandaConfigIni) => {
21
+ if (!pandaConfigIni) {
22
+ throw Error(`could not read panda config ${JSON.stringify(publicKeyLocation)}`);
23
+ }
24
+ else {
25
+ const config: { publicKey?: string } = iniparser.parseString(pandaConfigIni);
26
+ if (config.publicKey) {
27
+ return {
28
+ key: base64ToPEM(config.publicKey, "PUBLIC"),
29
+ lastUpdated: new Date()
30
+ };
31
+ } else {
32
+ console.log(`Failed to retrieve panda public key from ${JSON.stringify(config)}`);
33
+ throw new Error("Missing publicKey setting from config");
34
+ }
35
+ }
36
+ })
37
+ .catch((error) => {
38
+ console.error(`Error fetching public key from S3: ${error}`);
39
+ throw error;
40
+ });
25
41
  }
26
42
 
27
43
 
package/src/panda.ts CHANGED
@@ -3,6 +3,9 @@ import * as cookie from 'cookie';
3
3
  import {parseCookie, parseUser, sign, verifySignature} from './utils';
4
4
  import {User, AuthenticationResult, ValidateUserFn, gracePeriodInMillis} from './api';
5
5
  import { fetchPublicKey, PublicKeyHolder } from './fetch-public-key';
6
+ import { S3 } from "@aws-sdk/client-s3";
7
+ import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
8
+ import { AwsCredentialIdentityProvider } from "@aws-sdk/types";
6
9
 
7
10
  export function createCookie(user: User, privateKey: string): string {
8
11
  let queryParams: string[] = [];
@@ -103,15 +106,21 @@ export class PanDomainAuthentication {
103
106
  publicKey: Promise<PublicKeyHolder>;
104
107
  keyCacheTimeInMillis: number = 60 * 1000; // 1 minute
105
108
  keyUpdateTimer?: NodeJS.Timeout;
109
+ s3Client: S3;
106
110
 
107
- constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn) {
111
+ constructor(cookieName: string, region: string, bucket: string, keyFile: string, validateUser: ValidateUserFn, credentialsProvider: AwsCredentialIdentityProvider = fromNodeProviderChain()) {
108
112
  this.cookieName = cookieName;
109
113
  this.region = region;
110
114
  this.bucket = bucket;
111
115
  this.keyFile = keyFile;
112
116
  this.validateUser = validateUser;
113
117
 
114
- this.publicKey = fetchPublicKey(region, bucket, keyFile);
118
+ const standardAwsConfig = {
119
+ region: region,
120
+ credentials: credentialsProvider,
121
+ };
122
+ this.s3Client = new S3(standardAwsConfig);
123
+ this.publicKey = fetchPublicKey(this.s3Client, bucket, keyFile);
115
124
 
116
125
  this.keyUpdateTimer = setInterval(() => this.getPublicKey(), this.keyCacheTimeInMillis);
117
126
  }
@@ -129,7 +138,7 @@ export class PanDomainAuthentication {
129
138
  const diff = now.getTime() - lastUpdated.getTime();
130
139
 
131
140
  if(diff > this.keyCacheTimeInMillis) {
132
- this.publicKey = fetchPublicKey(this.region, this.bucket, this.keyFile);
141
+ this.publicKey = fetchPublicKey(this.s3Client, this.bucket, this.keyFile);
133
142
  return this.publicKey.then(({ key }) => key);
134
143
  } else {
135
144
  return key;
@@ -141,7 +150,6 @@ export class PanDomainAuthentication {
141
150
  return this.getPublicKey().then(publicKey => {
142
151
  const cookies = cookie.parse(requestCookies);
143
152
  const pandaCookie = cookies[this.cookieName];
144
-
145
153
  return verifyUser(pandaCookie, publicKey, new Date(), this.validateUser);
146
154
  });
147
155
  }
@@ -17,7 +17,7 @@ import {
17
17
  import {decodeBase64, parseCookie, ParsedCookie, parseUser} from "../src/utils";
18
18
 
19
19
  jest.mock('../src/fetch-public-key');
20
- jest.useFakeTimers('modern');
20
+ jest.useFakeTimers();
21
21
 
22
22
  function userFromCookie(cookie: string): User {
23
23
  // This function is only used to generate a `User` object from