@adobe/spacecat-shared-http-utils 1.6.6 → 1.6.8

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,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-http-utils-v1.6.8](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-http-utils-v1.6.7...@adobe/spacecat-shared-http-utils-v1.6.8) (2024-08-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add Scoped API Key docs ([#342](https://github.com/adobe/spacecat-shared/issues/342)) ([2fbf707](https://github.com/adobe/spacecat-shared/commit/2fbf707dfeff914dc47ae1f9860629873927e03e))
7
+
8
+ # [@adobe/spacecat-shared-http-utils-v1.6.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-http-utils-v1.6.6...@adobe/spacecat-shared-http-utils-v1.6.7) (2024-08-19)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add try-catch block to authentication manager ([#336](https://github.com/adobe/spacecat-shared/issues/336)) ([a6cf629](https://github.com/adobe/spacecat-shared/commit/a6cf6290e4b8330956fdbb80406523853b0a7b51))
14
+
1
15
  # [@adobe/spacecat-shared-http-utils-v1.6.6](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-http-utils-v1.6.5...@adobe/spacecat-shared-http-utils-v1.6.6) (2024-08-19)
2
16
 
3
17
 
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
- # Response Helper Functions.
1
+ # Spacecat Shared - HTTP Utilities
2
2
 
3
- A set of TypeScript functions for creating HTTP responses with standardized formats.
3
+ A set of TypeScript functions for creating HTTP responses with standardized formats, and classes for dealing with
4
+ authenticating HTTP requests.
4
5
 
5
6
  ## Table of Contents
6
7
 
@@ -76,6 +77,72 @@ Creates a response for a not found scenario with an error message and optional h
76
77
 
77
78
  Creates a response for an internal server error with an error message and optional headers.
78
79
 
80
+ ## Authentication
81
+
82
+ This package includes classes for dealing with authenticating HTTP requests.
83
+
84
+ ### ScopedApiKeyHandler
85
+
86
+ Scoped API keys are defined in the datalayer and can be used to authenticate requests to the Spacecat API. They employ
87
+ "scopes" to enable fine-grained access to resources. An example API key entity looks like this (`id` omitted):
88
+
89
+ ```
90
+ {
91
+ "name": "Example API Key",
92
+ "hashedApiKey": "4c806362b613f7496abf284146efd31da90e4b16169fe001841ca17290f427c4",
93
+ "createdAt": "2024-08-21T19:00:00.000Z",
94
+ "expiresAt": "2024-12-21T19:00:00.000Z",
95
+ "scopes": [
96
+ { "name": "imports.write" },
97
+ { "name": "imports.read" }
98
+ ]
99
+ }
100
+ ```
101
+
102
+ Key points on the above:
103
+ - `hashedApiKey` is the SHA-256 hash of the actual API key ("test-api-key" above)
104
+ - `scopes` are the permissions granted to the API key
105
+ - Each `scope` object can contain additional data, but the `name` field is required
106
+
107
+ The `ScopedApiKeyHandler` class is used to authenticate requests using scoped API keys. To support the existing
108
+ Legacy API keys, it should be ordered after the `LegacyApiKeyHandler` in the `authHandlers` array. This enables requests
109
+ with the existing API keys to be authenticated quickly without requiring a database lookup.
110
+
111
+ #### Checking for scope access
112
+
113
+ To enable a new scope, first refer to the `scopeNames` array in the ApiKey model (/packages/spacecat-shared-data-access/src/models/api-key.js).
114
+ If the scope you need is not listed here, please add it. Note the convention for scope names is `resource.action`,
115
+ e.g. `imports.write` or `sites.read_all`. The `_all` action suffix indicates access beyond resources created (or
116
+ jobs initiated by) the current API key.
117
+
118
+ Next, you will want to check that the API used to make the request has access to the required scope(s) from your
119
+ controller. The `authWrapper` adds an `auth` helper to the context which makes this easy. Here's an example of how to
120
+ check for scope access from a controller:
121
+
122
+ ```
123
+ // This route requires the 'imports.write' scope
124
+ function protectedRoute(context) {
125
+ const { auth } = context;
126
+
127
+ try {
128
+ auth.checkScopes(['imports.write']);
129
+ } catch (error) {
130
+ throw new ErrorWithStatusCode('Missing required scopes', 401);
131
+ }
132
+
133
+ return ok('You have access to this resource');
134
+ }
135
+ ```
136
+
137
+ Need additional details from the API key entity object? The `authWrapper` places the authenticated `authInfo` object
138
+ into the context at `context.attributes.authInfo`, with the API key entity available in its `profile` property.
139
+
140
+ #### Creating a new API key
141
+
142
+ This is currently a manual process, and involves duplicating an existing API key entity in the datalayer and updating
143
+ its properties. For the table to update, refer to the `TABLE_NAME_API_KEYS` constant (which will be overridden on prod).
144
+
145
+ In the future we are planning to support a way for clients to request their own API key, given a valid IMS token.
79
146
 
80
147
  ## Contributing
81
148
 
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-http-utils",
3
- "version": "1.6.6",
3
+ "version": "1.6.8",
4
4
  "description": "Shared modules of the Spacecat Services - HTTP Utils",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": "^20.0.0 <21.0.0",
8
+ "npm": "^10.0.0 <11.0.0"
9
+ },
6
10
  "main": "src/index.js",
7
11
  "types": "src/index.d.ts",
8
12
  "scripts": {
@@ -36,7 +40,7 @@
36
40
  },
37
41
  "devDependencies": {
38
42
  "@adobe/helix-shared-wrap": "2.0.2",
39
- "chai": "4.5.0",
43
+ "chai": "5.1.1",
40
44
  "chai-as-promised": "8.0.0",
41
45
  "sinon": "18.0.0"
42
46
  }
@@ -54,7 +54,7 @@ export function authWrapper(fn, opts = {}) {
54
54
  checkScopes: (scopes) => checkScopes(scopes, authInfo, log),
55
55
  };
56
56
  }
57
- } catch (error) {
57
+ } catch {
58
58
  return new Response('Unauthorized', { status: 401 });
59
59
  }
60
60
 
@@ -45,8 +45,13 @@ export default class AuthenticationManager {
45
45
  for (const handler of this.handlers) {
46
46
  this.log.debug(`Trying to authenticate with ${handler.name}`);
47
47
 
48
- // eslint-disable-next-line no-await-in-loop
49
- const authInfo = await handler.checkAuth(request, context);
48
+ let authInfo;
49
+ try {
50
+ // eslint-disable-next-line no-await-in-loop
51
+ authInfo = await handler.checkAuth(request, context);
52
+ } catch (error) {
53
+ this.log.error(`Failed to authenticate with ${handler.name}:`, error);
54
+ }
50
55
 
51
56
  if (isObject(authInfo)) {
52
57
  this.log.info(`Authenticated with ${handler.name}`);
@@ -29,17 +29,14 @@ export default class ScopedApiKeyHandler extends AbstractHandler {
29
29
  if (!dataAccess) {
30
30
  throw new Error('Data access is required');
31
31
  }
32
- this.log('Checking for API key in the request headers', 'debug');
33
32
 
34
33
  const apiKeyFromHeader = headers['x-api-key'];
35
34
  if (!hasText(apiKeyFromHeader)) {
36
35
  return null;
37
36
  }
38
37
 
39
- this.log(`Checking for API key: ${apiKeyFromHeader}`, 'debug');
40
38
  // Keys are stored by their hash, so we need to hash the key to look it up
41
39
  const hashedApiKey = hashWithSHA256(apiKeyFromHeader);
42
- this.log(`Checking for API key with hash: ${hashedApiKey}`, 'debug');
43
40
  const apiKeyEntity = await dataAccess.getApiKeyByHashedApiKey(hashedApiKey);
44
41
 
45
42
  if (!apiKeyEntity) {
@@ -52,7 +49,6 @@ export default class ScopedApiKeyHandler extends AbstractHandler {
52
49
  const authInfo = new AuthInfo()
53
50
  .withProfile(apiKeyEntity) // Include the API key entity as the profile
54
51
  .withType(this.name);
55
- this.log('Successfully constructed authInfo object', 'debug');
56
52
 
57
53
  // Verify that the api key has not expired or been revoked
58
54
  const now = new Date().toISOString();