lambda-essentials-ts 4.1.3 → 5.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 CHANGED
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
 
7
+ ## [5.0.0] - 2022-02-22
8
+
9
+ ### Changed
10
+
11
+ - **[Breaking change]** `TokenProvider` was replaced by more specific `KmsTokenProvider` class.
12
+ The functionality and interface remains the same, the imports need to be changed.
13
+
14
+ ### Added
15
+
16
+ - New `SecretsManagerTokenProvider` that relies on AWS Secrets Manager to retrieve client ID and client secret.
17
+ The advantage of using AWS Secrets Manager is that it can be supplied with a secret rotation function.
18
+
19
+ ## [4.1.5] - 2022-02-10
20
+
21
+ ### Changed
22
+
23
+ `ClientException` now maps `HTTP 422` client responses to `HTTP 422` server responses (was `HTTP 503` before).
24
+
7
25
  ## [4.1.2] - 2021-12-02
8
26
 
9
27
  ### Changed
package/README.md CHANGED
@@ -88,15 +88,55 @@ let axiosClient = axios.create({ timeout: 3000 });
88
88
  new HttpClient({ client: axiosClient });
89
89
  ```
90
90
 
91
- ### TokenProvider
91
+ ### SecretsManagerTokenProvider
92
+
93
+ It uses AWS Secrets Manager to retrieve the client ID and secret and then calls the specified token endpoint the retrieve JWT.
94
+
95
+ CloudFormation to create a secret. Also allow the lambda function to access the secret by attaching `AWSSecretsManagerGetSecretValuePolicy` IAM Policy.
96
+
97
+ ```yaml
98
+ Auth0Secret:
99
+ Type: AWS::SecretsManager::Secret
100
+ Properties:
101
+ Description: 'Auth0 Client ID/Secret'
102
+ GenerateSecretString:
103
+ SecretStringTemplate: '{"Auth0ClientID": "client_id", "Auth0ClientSecret": "client_secret"}'
104
+ GenerateStringKey: 'Auth0ClientSecret'
105
+ ```
106
+
107
+ ```typescript
108
+ import {
109
+ SecretsManagerTokenProvider,
110
+ SecretsManagerTokenConfiguration,
111
+ } from 'lambda-essentials-ts';
112
+
113
+ const configuration: SecretsManagerTokenConfiguration = {
114
+ clientSecretId: 'arn:aws:secretsmanager:eu-west-1:<aws_account_id>:secret:<secret_id>',
115
+ audience: 'https://example.com/',
116
+ tokenEndpoint: 'https://example.com/oauth/token',
117
+ };
118
+
119
+ const secretsManagerClient = new aws.SecretsManager({ region: 'eu-west-1' });
120
+ const tokenProvider = new SecretsManagerTokenProvider({
121
+ secretsManagerClient: secretsManagerClient,
122
+ tokenConfiguration: configuration,
123
+ });
124
+
125
+ // recommended way to retrieve token (utilizes caching and takes care of token expiration)
126
+ const accessToken = await tokenProvider.getToken();
127
+
128
+ // or bypass caching and get a new fresh token
129
+ const freshAccessToken = await tokenProvider.getTokenWithoutCache();
130
+ ```
131
+
132
+ ### KmsTokenProvider
92
133
 
93
134
  It uses AWS KMS to decrypt the client secret and then calls the specified token endpoint the retrieve JWT.
94
135
 
95
136
  ```typescript
96
- import axios from 'axios';
97
- import { TokenProvider, TokenConfiguration } from 'lambda-essentials-ts';
137
+ import { KmsTokenProvider, KmsTokenConfiguration } from 'lambda-essentials-ts';
98
138
 
99
- const configuration: TokenConfiguration = {
139
+ const configuration: KmsTokenConfiguration = {
100
140
  clientId: 'CLIENT_ID',
101
141
  encryptedClientSecret: 'BASE64_KMS_ENCRYPTED_CLIENT_SECRET',
102
142
  audience: 'https://example.com/',
@@ -104,17 +144,16 @@ const configuration: TokenConfiguration = {
104
144
  };
105
145
 
106
146
  const kmsClient = new aws.KMS({ region: 'eu-west-1' });
107
- const tokenProvider = new TokenProvider({
108
- httpClient: axios.create(),
147
+ const tokenProvider = new KmsTokenProvider({
109
148
  kmsClient: kmsClient,
110
149
  tokenConfiguration: configuration,
111
150
  });
112
151
 
113
152
  // recommended way to retrieve token (utilizes caching and takes care of token expiration)
114
- let accessToken = await tokenProvider.getToken();
153
+ const accessToken = await tokenProvider.getToken();
115
154
 
116
- // or bypass caching and get new token
117
- accessToken = await tokenProvider.getTokenWithoutCache();
155
+ // or bypass caching and get a new fresh token
156
+ const freshAccessToken = await tokenProvider.getTokenWithoutCache();
118
157
  ```
119
158
 
120
159
  ### Exceptions
@@ -22,4 +22,5 @@ ClientException.statusCodeMap = {
22
22
  401: 401,
23
23
  403: 403,
24
24
  404: 422,
25
+ 422: 422,
25
26
  };
package/lib/index.d.ts CHANGED
@@ -3,7 +3,9 @@ import Logger, { LoggerConfiguration, SuggestedLogObject } from './logger/logger
3
3
  import OpenApiWrapper from './openApi/openApiWrapper';
4
4
  import { ApiResponse } from './openApi/apiResponseModel';
5
5
  import { ApiRequest, GetRequest, PostRequest, PutRequest } from './openApi/apiRequestModel';
6
- import TokenProvider, { TokenConfiguration, TokenProviderOptions, TokenProviderHttpClient } from './tokenProvider/tokenProvider';
6
+ import { TokenProviderHttpClient } from './tokenProvider/tokenProvider';
7
+ import KmsTokenProvider, { KmsTokenProviderOptions, KmsTokenConfiguration } from './tokenProvider/kmsTokenProvider';
8
+ import SecretsManagerTokenProvider, { SecretsManagerTokenProviderOptions, SecretsManagerTokenConfiguration } from './tokenProvider/secretsManagerTokenProvider';
7
9
  import { ClientException } from './exceptions/clientException';
8
10
  import { ConflictException } from './exceptions/conflictException';
9
11
  import { Exception } from './exceptions/exception';
@@ -13,5 +15,5 @@ import { InvalidDataException } from './exceptions/invalidDataException';
13
15
  import { NotFoundException } from './exceptions/notFoundException';
14
16
  import { ValidationException } from './exceptions/validationException';
15
17
  import { serializeObject, serializeAxiosError } from './util';
16
- export { Logger, OpenApiWrapper, ApiResponse, HttpClient, HttpLogType, TokenProvider, ValidationException, NotFoundException, InvalidDataException, InternalException, ForbiddenException, Exception, ConflictException, ClientException, serializeObject, serializeAxiosError, };
17
- export type { LoggerConfiguration, SuggestedLogObject, HttpClientOptions, HttpLogOptions, ApiRequest, GetRequest, PostRequest, PutRequest, TokenConfiguration, TokenProviderOptions, TokenProviderHttpClient, };
18
+ export { Logger, OpenApiWrapper, ApiResponse, HttpClient, HttpLogType, KmsTokenProvider, SecretsManagerTokenProvider, ValidationException, NotFoundException, InvalidDataException, InternalException, ForbiddenException, Exception, ConflictException, ClientException, serializeObject, serializeAxiosError, };
19
+ export type { LoggerConfiguration, SuggestedLogObject, HttpClientOptions, HttpLogOptions, ApiRequest, GetRequest, PostRequest, PutRequest, TokenProviderHttpClient, KmsTokenProviderOptions, KmsTokenConfiguration, SecretsManagerTokenProviderOptions, SecretsManagerTokenConfiguration, };
package/lib/index.js CHANGED
@@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.serializeAxiosError = exports.serializeObject = exports.ClientException = exports.ConflictException = exports.Exception = exports.ForbiddenException = exports.InternalException = exports.InvalidDataException = exports.NotFoundException = exports.ValidationException = exports.TokenProvider = exports.HttpLogType = exports.HttpClient = exports.ApiResponse = exports.OpenApiWrapper = exports.Logger = void 0;
25
+ exports.serializeAxiosError = exports.serializeObject = exports.ClientException = exports.ConflictException = exports.Exception = exports.ForbiddenException = exports.InternalException = exports.InvalidDataException = exports.NotFoundException = exports.ValidationException = exports.SecretsManagerTokenProvider = exports.KmsTokenProvider = exports.HttpLogType = exports.HttpClient = exports.ApiResponse = exports.OpenApiWrapper = exports.Logger = void 0;
26
26
  const httpClient_1 = __importStar(require("./httpClient/httpClient"));
27
27
  exports.HttpClient = httpClient_1.default;
28
28
  Object.defineProperty(exports, "HttpLogType", { enumerable: true, get: function () { return httpClient_1.HttpLogType; } });
@@ -32,8 +32,10 @@ const openApiWrapper_1 = __importDefault(require("./openApi/openApiWrapper"));
32
32
  exports.OpenApiWrapper = openApiWrapper_1.default;
33
33
  const apiResponseModel_1 = require("./openApi/apiResponseModel");
34
34
  Object.defineProperty(exports, "ApiResponse", { enumerable: true, get: function () { return apiResponseModel_1.ApiResponse; } });
35
- const tokenProvider_1 = __importDefault(require("./tokenProvider/tokenProvider"));
36
- exports.TokenProvider = tokenProvider_1.default;
35
+ const kmsTokenProvider_1 = __importDefault(require("./tokenProvider/kmsTokenProvider"));
36
+ exports.KmsTokenProvider = kmsTokenProvider_1.default;
37
+ const secretsManagerTokenProvider_1 = __importDefault(require("./tokenProvider/secretsManagerTokenProvider"));
38
+ exports.SecretsManagerTokenProvider = secretsManagerTokenProvider_1.default;
37
39
  const clientException_1 = require("./exceptions/clientException");
38
40
  Object.defineProperty(exports, "ClientException", { enumerable: true, get: function () { return clientException_1.ClientException; } });
39
41
  const conflictException_1 = require("./exceptions/conflictException");
@@ -13,7 +13,7 @@ export interface ApiRequest<Body = any, Query = any> {
13
13
  headers: Record<string, string>;
14
14
  pathParameters: Record<string, string>;
15
15
  queryStringParameters?: Query;
16
- multiValueQueryStringParameters?: Query;
16
+ multiValueQueryStringParameters?: any;
17
17
  }
18
18
  export interface PostRequest<Body = any, Query = any> extends ApiRequest<Body, Query> {
19
19
  body: Body;
@@ -0,0 +1,28 @@
1
+ import { KMS } from 'aws-sdk';
2
+ import TokenProvider, { Auth0Secret, TokenConfiguration, TokenProviderOptions } from './tokenProvider';
3
+ export default class KmsTokenProvider extends TokenProvider {
4
+ private kmsClient;
5
+ private kmsConfiguration;
6
+ constructor(options: KmsTokenProviderOptions);
7
+ getClientSecret(): Promise<Auth0Secret | undefined>;
8
+ }
9
+ export interface KmsTokenProviderOptions extends TokenProviderOptions {
10
+ /**
11
+ * AWS KMS Client
12
+ */
13
+ kmsClient: KMS;
14
+ /**
15
+ * Configuration needed for the token
16
+ */
17
+ tokenConfiguration: KmsTokenConfiguration;
18
+ }
19
+ export interface KmsTokenConfiguration extends TokenConfiguration {
20
+ /**
21
+ * Username or ClientId
22
+ */
23
+ clientId: string;
24
+ /**
25
+ * KMS Encrypted client secret
26
+ */
27
+ encryptedClientSecret: string;
28
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const tokenProvider_1 = __importDefault(require("./tokenProvider"));
7
+ class KmsTokenProvider extends tokenProvider_1.default {
8
+ constructor(options) {
9
+ super(options);
10
+ this.kmsClient = options.kmsClient;
11
+ this.kmsConfiguration = options.tokenConfiguration;
12
+ }
13
+ async getClientSecret() {
14
+ const secret = await this.kmsClient
15
+ .decrypt({
16
+ CiphertextBlob: Buffer.from(this.kmsConfiguration.encryptedClientSecret, 'base64'),
17
+ })
18
+ .promise()
19
+ .then((data) => { var _a; return (_a = data.Plaintext) === null || _a === void 0 ? void 0 : _a.toString(); });
20
+ if (!secret) {
21
+ throw new Error('Request error: failed to decrypt secret using KMS');
22
+ }
23
+ return { Auth0ClientSecret: secret, Auth0ClientID: this.kmsConfiguration.clientId };
24
+ }
25
+ }
26
+ exports.default = KmsTokenProvider;
@@ -0,0 +1,29 @@
1
+ import { SecretsManager } from 'aws-sdk';
2
+ import TokenProvider, { Auth0Secret, TokenConfiguration, TokenProviderOptions } from './tokenProvider';
3
+ export default class SecretsManagerTokenProvider extends TokenProvider {
4
+ private secretsManagerClient;
5
+ private secretsManagerConfiguration;
6
+ constructor(options: SecretsManagerTokenProviderOptions);
7
+ getClientSecret(): Promise<Auth0Secret | undefined>;
8
+ }
9
+ export interface SecretsManagerTokenProviderOptions extends TokenProviderOptions {
10
+ /**
11
+ * AWS Secrets Manager Client
12
+ */
13
+ secretsManagerClient: SecretsManager;
14
+ /**
15
+ * Configuration needed for the token
16
+ */
17
+ tokenConfiguration: SecretsManagerTokenConfiguration;
18
+ }
19
+ export interface SecretsManagerTokenConfiguration extends TokenConfiguration {
20
+ /**
21
+ * The ID of a secret stored in AWS Secrets Manager.
22
+ * The expected secret format is:
23
+ * {
24
+ * Auth0ClientID: "string",
25
+ * Auth0ClientSecret: "string"
26
+ * }
27
+ */
28
+ clientSecretId: string;
29
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const tokenProvider_1 = __importDefault(require("./tokenProvider"));
7
+ class SecretsManagerTokenProvider extends tokenProvider_1.default {
8
+ constructor(options) {
9
+ super(options);
10
+ this.secretsManagerClient = options.secretsManagerClient;
11
+ this.secretsManagerConfiguration = options.tokenConfiguration;
12
+ }
13
+ async getClientSecret() {
14
+ const secret = await this.secretsManagerClient
15
+ .getSecretValue({ SecretId: this.secretsManagerConfiguration.clientSecretId })
16
+ .promise();
17
+ if (!(secret === null || secret === void 0 ? void 0 : secret.SecretString)) {
18
+ throw new Error('Request error: failed to retrieve secret from Secrets Manager');
19
+ }
20
+ return JSON.parse(secret.SecretString);
21
+ }
22
+ }
23
+ exports.default = SecretsManagerTokenProvider;
@@ -1,14 +1,12 @@
1
- import { KMS } from 'aws-sdk';
2
1
  import { AxiosRequestConfig, AxiosResponse } from 'axios';
3
- export default class TokenProvider {
2
+ export default abstract class TokenProvider {
4
3
  private httpClient;
5
- private kmsClient;
6
4
  private configuration;
7
5
  private currentTokenPromise?;
8
6
  /**
9
7
  * Create a new Instance of the TokenProvider
10
8
  */
11
- constructor(options: TokenProviderOptions);
9
+ protected constructor(options: TokenProviderOptions);
12
10
  /**
13
11
  * Get access token. Subsequent calls are cached and the token is renewed only if it is expired.
14
12
  */
@@ -17,30 +15,23 @@ export default class TokenProvider {
17
15
  * Get access token.
18
16
  */
19
17
  getTokenWithoutCache(): Promise<string>;
18
+ abstract getClientSecret(): Promise<Auth0Secret | undefined>;
19
+ }
20
+ export interface Auth0Secret {
21
+ Auth0ClientID: string;
22
+ Auth0ClientSecret: string;
20
23
  }
21
24
  export interface TokenProviderOptions {
22
25
  /**
23
26
  * Either an Axios instance or an @atsquad/httpclient instance, by default it creates an axios instance
24
27
  */
25
28
  httpClient?: TokenProviderHttpClient;
26
- /**
27
- * AWS KMS Client
28
- */
29
- kmsClient: KMS;
30
29
  /**
31
30
  * Configuration needed for the token
32
31
  */
33
32
  tokenConfiguration: TokenConfiguration;
34
33
  }
35
34
  export interface TokenConfiguration {
36
- /**
37
- * Username or ClientId
38
- */
39
- clientId: string;
40
- /**
41
- * KMS Encrypted client secret
42
- */
43
- encryptedClientSecret: string;
44
35
  audience: string;
45
36
  tokenEndpoint: string;
46
37
  }
@@ -12,24 +12,7 @@ class TokenProvider {
12
12
  constructor(options) {
13
13
  var _a;
14
14
  this.httpClient = (_a = options.httpClient) !== null && _a !== void 0 ? _a : axios_1.default.create();
15
- if (!options.kmsClient) {
16
- throw new Error('KMS Client is missing');
17
- }
18
- this.kmsClient = options.kmsClient;
19
- const configuration = options.tokenConfiguration;
20
- if (!(configuration === null || configuration === void 0 ? void 0 : configuration.clientId)) {
21
- throw new Error('Configuration error: missing required property "clientId"');
22
- }
23
- if (!(configuration === null || configuration === void 0 ? void 0 : configuration.encryptedClientSecret)) {
24
- throw new Error('Configuration error: missing required property "encryptedClientSecret"');
25
- }
26
- if (!(configuration === null || configuration === void 0 ? void 0 : configuration.audience)) {
27
- throw new Error('Configuration error: missing required property "audience"');
28
- }
29
- if (!(configuration === null || configuration === void 0 ? void 0 : configuration.tokenEndpoint)) {
30
- throw new Error('Configuration error: missing required property "tokenEndpoint"');
31
- }
32
- this.configuration = configuration;
15
+ this.configuration = options.tokenConfiguration;
33
16
  }
34
17
  /**
35
18
  * Get access token. Subsequent calls are cached and the token is renewed only if it is expired.
@@ -43,10 +26,10 @@ class TokenProvider {
43
26
  try {
44
27
  const jwtToken = await this.currentTokenPromise;
45
28
  // lower the token expiry time by 10s so that the returned token will be not immediately expired
46
- if (!jwtToken || ((_a = jsonwebtoken_1.default.decode(jwtToken)) === null || _a === void 0 ? void 0 : _a['exp']) < Date.now() / 1000 - 10) {
29
+ if (!jwtToken || ((_a = jsonwebtoken_1.default.decode(jwtToken)) === null || _a === void 0 ? void 0 : _a.exp) < Date.now() / 1000 - 10) {
47
30
  this.currentTokenPromise = this.getTokenWithoutCache();
48
31
  }
49
- return this.currentTokenPromise;
32
+ return await this.currentTokenPromise;
50
33
  }
51
34
  catch (error) {
52
35
  this.currentTokenPromise = this.getTokenWithoutCache();
@@ -57,14 +40,14 @@ class TokenProvider {
57
40
  * Get access token.
58
41
  */
59
42
  async getTokenWithoutCache() {
60
- const secret = await this.kmsClient
61
- .decrypt({ CiphertextBlob: Buffer.from(this.configuration.encryptedClientSecret, 'base64') })
62
- .promise()
63
- .then((data) => { var _a; return (_a = data.Plaintext) === null || _a === void 0 ? void 0 : _a.toString(); });
43
+ const secret = await this.getClientSecret();
44
+ if (!(secret === null || secret === void 0 ? void 0 : secret.Auth0ClientID) || !secret.Auth0ClientSecret) {
45
+ throw new Error('Request error: failed to retrieve Auth0 Client ID/Secret');
46
+ }
64
47
  const headers = { 'Content-Type': 'application/json' };
65
48
  const body = {
66
- client_id: this.configuration.clientId,
67
- client_secret: secret,
49
+ client_id: secret.Auth0ClientID,
50
+ client_secret: secret.Auth0ClientSecret,
68
51
  audience: this.configuration.audience,
69
52
  grant_type: 'client_credentials',
70
53
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambda-essentials-ts",
3
- "version": "4.1.3",
3
+ "version": "5.0.0",
4
4
  "description": "A selection of the finest modules supporting authorization, API routing, error handling, logging and sending HTTP requests.",
5
5
  "main": "lib/index.js",
6
6
  "private": false,
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "homepage": "https://github.com/Cimpress-MCP/lambda-essentials-ts#readme",
28
28
  "dependencies": {
29
- "aws-sdk": "2.939.0",
29
+ "aws-sdk": "2.1078.0",
30
30
  "axios": "0.21.1",
31
31
  "axios-cache-adapter": "2.7.3",
32
32
  "fast-safe-stringify": "2.0.7",