@commercetools-backend/express 0.0.0-CRAFT-1791-20251006162610
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/commercetools-backend-express.cjs.d.ts +2 -0
- package/dist/commercetools-backend-express.cjs.dev.js +237 -0
- package/dist/commercetools-backend-express.cjs.js +7 -0
- package/dist/commercetools-backend-express.cjs.prod.js +237 -0
- package/dist/commercetools-backend-express.esm.js +221 -0
- package/dist/declarations/src/auth.d.ts +12 -0
- package/dist/declarations/src/constants.d.ts +34 -0
- package/dist/declarations/src/index.d.ts +3 -0
- package/dist/declarations/src/middlewares/session-middleware.d.ts +4 -0
- package/dist/declarations/src/types.d.ts +53 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) commercetools GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @commercetools-backend/express
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://www.npmjs.com/package/@commercetools-backend/express"><img src="https://badgen.net/npm/v/@commercetools-backend/express" alt="Latest release (latest dist-tag)" /></a> <a href="https://www.npmjs.com/package/@commercetools-backend/express"><img src="https://badgen.net/npm/v/@commercetools-backend/express/next" alt="Latest release (next dist-tag)" /></a> <a href="https://bundlephobia.com/result?p=@commercetools-backend/express"><img src="https://badgen.net/bundlephobia/minzip/@commercetools-backend/express" alt="Minified + GZipped size" /></a> <a href="https://github.com/commercetools/merchant-center-application-kit/blob/main/LICENSE"><img src="https://badgen.net/github/license/commercetools/merchant-center-application-kit" alt="GitHub license" /></a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Zero-config HTTP server as Express.js to facilitate development.
|
|
8
|
+
|
|
9
|
+
This package is primarily built for HTTP servers used by Custom Applications and it provides a set of components to facilitate the development of the HTTP server.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
$ npm install --save @commercetools-backend/express
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Check out the [documentation](https://docs.commercetools.com/merchant-center-customizations/concepts/integrate-with-your-own-api) for more information.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export * from "./declarations/src/index.js";
|
|
2
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29tbWVyY2V0b29scy1iYWNrZW5kLWV4cHJlc3MuY2pzLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuL2RlY2xhcmF0aW9ucy9zcmMvaW5kZXguZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSJ9
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var _URL = require('@babel/runtime-corejs3/core-js-stable/url');
|
|
6
|
+
var _Map = require('@babel/runtime-corejs3/core-js-stable/map');
|
|
7
|
+
var _startsWithInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/starts-with');
|
|
8
|
+
var jose = require('jose');
|
|
9
|
+
var _slicedToArray = require('@babel/runtime-corejs3/helpers/slicedToArray');
|
|
10
|
+
var _findInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/find');
|
|
11
|
+
var _Object$entries = require('@babel/runtime-corejs3/core-js-stable/object/entries');
|
|
12
|
+
var _Array$isArray = require('@babel/runtime-corejs3/core-js-stable/array/is-array');
|
|
13
|
+
|
|
14
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
|
15
|
+
|
|
16
|
+
var _URL__default = /*#__PURE__*/_interopDefault(_URL);
|
|
17
|
+
var _Map__default = /*#__PURE__*/_interopDefault(_Map);
|
|
18
|
+
var _startsWithInstanceProperty__default = /*#__PURE__*/_interopDefault(_startsWithInstanceProperty);
|
|
19
|
+
var _findInstanceProperty__default = /*#__PURE__*/_interopDefault(_findInstanceProperty);
|
|
20
|
+
var _Object$entries__default = /*#__PURE__*/_interopDefault(_Object$entries);
|
|
21
|
+
var _Array$isArray__default = /*#__PURE__*/_interopDefault(_Array$isArray);
|
|
22
|
+
|
|
23
|
+
const CLOUD_IDENTIFIERS = {
|
|
24
|
+
GCP_AU: 'gcp-au',
|
|
25
|
+
GCP_EU: 'gcp-eu',
|
|
26
|
+
GCP_US: 'gcp-us',
|
|
27
|
+
AWS_EU: 'aws-eu',
|
|
28
|
+
AWS_US: 'aws-us',
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated: use `AWS_EU` instead
|
|
31
|
+
*/
|
|
32
|
+
AWS_FRA: 'aws-fra',
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated: use `AWS_US` instead
|
|
35
|
+
*/
|
|
36
|
+
AWS_OHIO: 'aws-ohio'
|
|
37
|
+
};
|
|
38
|
+
const MC_API_URLS = {
|
|
39
|
+
[CLOUD_IDENTIFIERS.GCP_AU]: 'https://mc-api.australia-southeast1.gcp.commercetools.com',
|
|
40
|
+
[CLOUD_IDENTIFIERS.GCP_EU]: 'https://mc-api.europe-west1.gcp.commercetools.com',
|
|
41
|
+
[CLOUD_IDENTIFIERS.GCP_US]: 'https://mc-api.us-central1.gcp.commercetools.com',
|
|
42
|
+
[CLOUD_IDENTIFIERS.AWS_EU]: 'https://mc-api.eu-central-1.aws.commercetools.com',
|
|
43
|
+
[CLOUD_IDENTIFIERS.AWS_US]: 'https://mc-api.us-east-2.aws.commercetools.com',
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated: use `AWS_EU` instead
|
|
46
|
+
*/
|
|
47
|
+
[CLOUD_IDENTIFIERS.AWS_FRA]: 'https://mc-api.eu-central-1.aws.commercetools.com',
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated: use `AWS_US` instead
|
|
50
|
+
*/
|
|
51
|
+
[CLOUD_IDENTIFIERS.AWS_OHIO]: 'https://mc-api.us-east-2.aws.commercetools.com'
|
|
52
|
+
};
|
|
53
|
+
const MC_API_PROXY_HEADERS = {
|
|
54
|
+
FORWARD_TO_VERSION: 'x-mc-api-forward-to-version',
|
|
55
|
+
CLOUD_IDENTIFIER: 'x-mc-api-cloud-identifier'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getHeaderByCaseInsensitiveKey = (headers, headerKey) => {
|
|
59
|
+
var _context;
|
|
60
|
+
const matchingHeader = _findInstanceProperty__default["default"](_context = _Object$entries__default["default"](headers)).call(_context, _ref => {
|
|
61
|
+
let _ref2 = _slicedToArray(_ref, 1),
|
|
62
|
+
key = _ref2[0];
|
|
63
|
+
return headerKey.toLowerCase() === key.toLowerCase();
|
|
64
|
+
});
|
|
65
|
+
if (matchingHeader && matchingHeader.length > 0) {
|
|
66
|
+
const _matchingHeader = _slicedToArray(matchingHeader, 2),
|
|
67
|
+
headerValue = _matchingHeader[1];
|
|
68
|
+
return _Array$isArray__default["default"](headerValue) ? headerValue[0] : headerValue;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
const getFirstHeaderValueOrThrow = (headers, headerKey, errorMessage) => {
|
|
73
|
+
const headerValue = getHeaderByCaseInsensitiveKey(headers, headerKey);
|
|
74
|
+
if (!headerValue) {
|
|
75
|
+
throw new Error(errorMessage);
|
|
76
|
+
}
|
|
77
|
+
return headerValue;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Assign a session object to the request object.
|
|
81
|
+
const writeSessionContext = (request, verifiedToken) => {
|
|
82
|
+
const publicClaimForProjectKey = `${verifiedToken.iss}/claims/project_key`;
|
|
83
|
+
const publicClaimForUserPermissionsKey = `${verifiedToken.iss}/claims/user_permissions`;
|
|
84
|
+
request.session = {
|
|
85
|
+
userId: verifiedToken.sub,
|
|
86
|
+
projectKey: verifiedToken[publicClaimForProjectKey]
|
|
87
|
+
};
|
|
88
|
+
const userPermissions = verifiedToken[publicClaimForUserPermissionsKey];
|
|
89
|
+
if (Boolean(userPermissions?.length)) {
|
|
90
|
+
request.session.userPermissions = userPermissions;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Given a cloud identifier, try to map it to one of the supported
|
|
95
|
+
// environments and return the MC API URL for that environment.
|
|
96
|
+
// The URL points to the new hostnames.
|
|
97
|
+
// https://docs.commercetools.com/merchant-center-customizations/concepts/merchant-center-api#hostnames
|
|
98
|
+
const mapCloudIdentifierToIssuer = issuer => {
|
|
99
|
+
switch (issuer) {
|
|
100
|
+
case CLOUD_IDENTIFIERS.GCP_AU:
|
|
101
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_AU];
|
|
102
|
+
case CLOUD_IDENTIFIERS.GCP_EU:
|
|
103
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_EU];
|
|
104
|
+
case CLOUD_IDENTIFIERS.GCP_US:
|
|
105
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_US];
|
|
106
|
+
case CLOUD_IDENTIFIERS.AWS_EU:
|
|
107
|
+
case CLOUD_IDENTIFIERS.AWS_FRA:
|
|
108
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.AWS_EU];
|
|
109
|
+
case CLOUD_IDENTIFIERS.AWS_US:
|
|
110
|
+
case CLOUD_IDENTIFIERS.AWS_OHIO:
|
|
111
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.AWS_US];
|
|
112
|
+
default:
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// Given a cloud identifier, try to map it to a legacy hostname.
|
|
117
|
+
// This is for backwards compatibility.
|
|
118
|
+
const mapToLegacyIssuer = cloudIdentifier => {
|
|
119
|
+
switch (cloudIdentifier) {
|
|
120
|
+
case CLOUD_IDENTIFIERS.GCP_EU:
|
|
121
|
+
return 'https://mc-api.commercetools.com';
|
|
122
|
+
case CLOUD_IDENTIFIERS.GCP_US:
|
|
123
|
+
return 'https://mc-api.commercetools.co';
|
|
124
|
+
default:
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// Verifies that the issuer is a valid URL.
|
|
129
|
+
const throwIfIssuerIsNotAValidUrl = issuer => {
|
|
130
|
+
try {
|
|
131
|
+
new _URL__default["default"](issuer);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error(`Invalid issuer URL "${issuer}". Expected a valid URL to the Merchant Center API Gateway, or a cloud identifier to one of the available cloud regions. See https://docs.commercetools.com/merchant-center-customizations/concepts/merchant-center-api#hostnames.`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
// Validates required option values.
|
|
137
|
+
const validateRequiredValues = options => {
|
|
138
|
+
if (!options.audience) {
|
|
139
|
+
throw new Error(`Missing required option "audience"`);
|
|
140
|
+
}
|
|
141
|
+
if (!options.issuer) {
|
|
142
|
+
throw new Error(`Missing required option "issuer"`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
// Attempt to parse the given issuer. If the value is a cloud identifier, it will
|
|
146
|
+
// be mapped to one of the supported values. If not, we assume the value is a valid URL.
|
|
147
|
+
const getConfiguredDefaultIssuer = options => {
|
|
148
|
+
const issuer = mapCloudIdentifierToIssuer(options.issuer);
|
|
149
|
+
if (!issuer) {
|
|
150
|
+
throwIfIssuerIsNotAValidUrl(options.issuer);
|
|
151
|
+
return options.issuer;
|
|
152
|
+
}
|
|
153
|
+
return issuer;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Construct the audience from the given option + the request path.
|
|
157
|
+
// If the request path is `/`, do not append it to the audience, otherwise
|
|
158
|
+
// the token validation might fail because of mismatching audiences.
|
|
159
|
+
const getConfiguredAudience = (options, requestPath) => {
|
|
160
|
+
// remove the trailing slash
|
|
161
|
+
const url = new _URL__default["default"](`${options.audience.replace(/\/?$/, '')}${requestPath}`);
|
|
162
|
+
switch (options.audiencePolicy) {
|
|
163
|
+
case 'forward-url-origin':
|
|
164
|
+
return url.origin;
|
|
165
|
+
default:
|
|
166
|
+
{
|
|
167
|
+
if (requestPath === '/') {
|
|
168
|
+
return url.origin;
|
|
169
|
+
}
|
|
170
|
+
return `${url.origin}${url.pathname}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const jwksClientByIssuer = new _Map__default["default"]();
|
|
175
|
+
function getJwksClientByIssuer(issuer) {
|
|
176
|
+
const client = jwksClientByIssuer.get(issuer);
|
|
177
|
+
if (client) {
|
|
178
|
+
return client;
|
|
179
|
+
}
|
|
180
|
+
const newClient = jose.createRemoteJWKSet(new _URL__default["default"](`/.well-known/jwks.json`, issuer));
|
|
181
|
+
jwksClientByIssuer.set(issuer, newClient);
|
|
182
|
+
return newClient;
|
|
183
|
+
}
|
|
184
|
+
function createSessionAuthVerifier(options) {
|
|
185
|
+
validateRequiredValues(options);
|
|
186
|
+
const configuredDefaultIssuer = getConfiguredDefaultIssuer(options);
|
|
187
|
+
|
|
188
|
+
// Returns an async HTTP handler.
|
|
189
|
+
return async request => {
|
|
190
|
+
// Get the cloud identifier header, forwarded by the `/proxy/forward-to` endpoint.
|
|
191
|
+
const cloudIdentifierHeader = getFirstHeaderValueOrThrow(request.headers, MC_API_PROXY_HEADERS.CLOUD_IDENTIFIER, `Missing "X-MC-API-Cloud-Identifier" header.`);
|
|
192
|
+
let issuer = options.inferIssuer && cloudIdentifierHeader ? mapCloudIdentifierToIssuer(cloudIdentifierHeader) ?? configuredDefaultIssuer : configuredDefaultIssuer;
|
|
193
|
+
|
|
194
|
+
// Get the `Accept-version` header, forwarded by the `/proxy/forward-to` endpoint.
|
|
195
|
+
// The version should be sent by the client making the request, to use the features of v2.
|
|
196
|
+
const proxyForwardVersion = getFirstHeaderValueOrThrow(request.headers, MC_API_PROXY_HEADERS.FORWARD_TO_VERSION, `Missing "X-MC-API-Forward-To-Version" header.`);
|
|
197
|
+
if (proxyForwardVersion === 'v1') {
|
|
198
|
+
// Fall back to legacy issuer domains
|
|
199
|
+
issuer = mapToLegacyIssuer(cloudIdentifierHeader) ?? issuer;
|
|
200
|
+
}
|
|
201
|
+
const requestUrlPath = options.getRequestUrl ? options.getRequestUrl(request) : request.originalUrl ?? request.url;
|
|
202
|
+
if (!requestUrlPath || !_startsWithInstanceProperty__default["default"](requestUrlPath).call(requestUrlPath, '/')) {
|
|
203
|
+
throw new Error(`Invalid request URI path "${requestUrlPath}". Please make sure that the "request" object has either a property "originalUrl" or "url". If not, you should implement the "getRequestUrl" function and make sure to return a valid URI path value starting with "/". More info at https://docs.commercetools.com/merchant-center-customizations/concepts/integrate-with-your-own-api#validating-the-json-web-token`);
|
|
204
|
+
}
|
|
205
|
+
const audience = getConfiguredAudience(options, requestUrlPath);
|
|
206
|
+
const authorizationHeader = request.headers['authorization'];
|
|
207
|
+
if (typeof authorizationHeader !== 'string') {
|
|
208
|
+
throw new Error(`Missing "authorization" header`);
|
|
209
|
+
}
|
|
210
|
+
const exchangeToken = authorizationHeader.replace(/^Bearer (.*)$/, '$1');
|
|
211
|
+
const jwksClient = getJwksClientByIssuer(issuer);
|
|
212
|
+
const verifiedToken = await jose.jwtVerify(exchangeToken, jwksClient, {
|
|
213
|
+
algorithms: ['RS256'],
|
|
214
|
+
audience,
|
|
215
|
+
issuer
|
|
216
|
+
});
|
|
217
|
+
writeSessionContext(request, verifiedToken.payload);
|
|
218
|
+
return;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function createSessionMiddleware(options) {
|
|
223
|
+
const sessionAuthVerifier = createSessionAuthVerifier(options);
|
|
224
|
+
return async (request, _response, next) => {
|
|
225
|
+
try {
|
|
226
|
+
await sessionAuthVerifier(request);
|
|
227
|
+
next();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
next(error);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
exports.CLOUD_IDENTIFIERS = CLOUD_IDENTIFIERS;
|
|
235
|
+
exports.MC_API_URLS = MC_API_URLS;
|
|
236
|
+
exports.createSessionAuthVerifier = createSessionAuthVerifier;
|
|
237
|
+
exports.createSessionMiddleware = createSessionMiddleware;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var _URL = require('@babel/runtime-corejs3/core-js-stable/url');
|
|
6
|
+
var _Map = require('@babel/runtime-corejs3/core-js-stable/map');
|
|
7
|
+
var _startsWithInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/starts-with');
|
|
8
|
+
var jose = require('jose');
|
|
9
|
+
var _slicedToArray = require('@babel/runtime-corejs3/helpers/slicedToArray');
|
|
10
|
+
var _findInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/find');
|
|
11
|
+
var _Object$entries = require('@babel/runtime-corejs3/core-js-stable/object/entries');
|
|
12
|
+
var _Array$isArray = require('@babel/runtime-corejs3/core-js-stable/array/is-array');
|
|
13
|
+
|
|
14
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
|
15
|
+
|
|
16
|
+
var _URL__default = /*#__PURE__*/_interopDefault(_URL);
|
|
17
|
+
var _Map__default = /*#__PURE__*/_interopDefault(_Map);
|
|
18
|
+
var _startsWithInstanceProperty__default = /*#__PURE__*/_interopDefault(_startsWithInstanceProperty);
|
|
19
|
+
var _findInstanceProperty__default = /*#__PURE__*/_interopDefault(_findInstanceProperty);
|
|
20
|
+
var _Object$entries__default = /*#__PURE__*/_interopDefault(_Object$entries);
|
|
21
|
+
var _Array$isArray__default = /*#__PURE__*/_interopDefault(_Array$isArray);
|
|
22
|
+
|
|
23
|
+
const CLOUD_IDENTIFIERS = {
|
|
24
|
+
GCP_AU: 'gcp-au',
|
|
25
|
+
GCP_EU: 'gcp-eu',
|
|
26
|
+
GCP_US: 'gcp-us',
|
|
27
|
+
AWS_EU: 'aws-eu',
|
|
28
|
+
AWS_US: 'aws-us',
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated: use `AWS_EU` instead
|
|
31
|
+
*/
|
|
32
|
+
AWS_FRA: 'aws-fra',
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated: use `AWS_US` instead
|
|
35
|
+
*/
|
|
36
|
+
AWS_OHIO: 'aws-ohio'
|
|
37
|
+
};
|
|
38
|
+
const MC_API_URLS = {
|
|
39
|
+
[CLOUD_IDENTIFIERS.GCP_AU]: 'https://mc-api.australia-southeast1.gcp.commercetools.com',
|
|
40
|
+
[CLOUD_IDENTIFIERS.GCP_EU]: 'https://mc-api.europe-west1.gcp.commercetools.com',
|
|
41
|
+
[CLOUD_IDENTIFIERS.GCP_US]: 'https://mc-api.us-central1.gcp.commercetools.com',
|
|
42
|
+
[CLOUD_IDENTIFIERS.AWS_EU]: 'https://mc-api.eu-central-1.aws.commercetools.com',
|
|
43
|
+
[CLOUD_IDENTIFIERS.AWS_US]: 'https://mc-api.us-east-2.aws.commercetools.com',
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated: use `AWS_EU` instead
|
|
46
|
+
*/
|
|
47
|
+
[CLOUD_IDENTIFIERS.AWS_FRA]: 'https://mc-api.eu-central-1.aws.commercetools.com',
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated: use `AWS_US` instead
|
|
50
|
+
*/
|
|
51
|
+
[CLOUD_IDENTIFIERS.AWS_OHIO]: 'https://mc-api.us-east-2.aws.commercetools.com'
|
|
52
|
+
};
|
|
53
|
+
const MC_API_PROXY_HEADERS = {
|
|
54
|
+
FORWARD_TO_VERSION: 'x-mc-api-forward-to-version',
|
|
55
|
+
CLOUD_IDENTIFIER: 'x-mc-api-cloud-identifier'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const getHeaderByCaseInsensitiveKey = (headers, headerKey) => {
|
|
59
|
+
var _context;
|
|
60
|
+
const matchingHeader = _findInstanceProperty__default["default"](_context = _Object$entries__default["default"](headers)).call(_context, _ref => {
|
|
61
|
+
let _ref2 = _slicedToArray(_ref, 1),
|
|
62
|
+
key = _ref2[0];
|
|
63
|
+
return headerKey.toLowerCase() === key.toLowerCase();
|
|
64
|
+
});
|
|
65
|
+
if (matchingHeader && matchingHeader.length > 0) {
|
|
66
|
+
const _matchingHeader = _slicedToArray(matchingHeader, 2),
|
|
67
|
+
headerValue = _matchingHeader[1];
|
|
68
|
+
return _Array$isArray__default["default"](headerValue) ? headerValue[0] : headerValue;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
const getFirstHeaderValueOrThrow = (headers, headerKey, errorMessage) => {
|
|
73
|
+
const headerValue = getHeaderByCaseInsensitiveKey(headers, headerKey);
|
|
74
|
+
if (!headerValue) {
|
|
75
|
+
throw new Error(errorMessage);
|
|
76
|
+
}
|
|
77
|
+
return headerValue;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Assign a session object to the request object.
|
|
81
|
+
const writeSessionContext = (request, verifiedToken) => {
|
|
82
|
+
const publicClaimForProjectKey = `${verifiedToken.iss}/claims/project_key`;
|
|
83
|
+
const publicClaimForUserPermissionsKey = `${verifiedToken.iss}/claims/user_permissions`;
|
|
84
|
+
request.session = {
|
|
85
|
+
userId: verifiedToken.sub,
|
|
86
|
+
projectKey: verifiedToken[publicClaimForProjectKey]
|
|
87
|
+
};
|
|
88
|
+
const userPermissions = verifiedToken[publicClaimForUserPermissionsKey];
|
|
89
|
+
if (Boolean(userPermissions?.length)) {
|
|
90
|
+
request.session.userPermissions = userPermissions;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Given a cloud identifier, try to map it to one of the supported
|
|
95
|
+
// environments and return the MC API URL for that environment.
|
|
96
|
+
// The URL points to the new hostnames.
|
|
97
|
+
// https://docs.commercetools.com/merchant-center-customizations/concepts/merchant-center-api#hostnames
|
|
98
|
+
const mapCloudIdentifierToIssuer = issuer => {
|
|
99
|
+
switch (issuer) {
|
|
100
|
+
case CLOUD_IDENTIFIERS.GCP_AU:
|
|
101
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_AU];
|
|
102
|
+
case CLOUD_IDENTIFIERS.GCP_EU:
|
|
103
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_EU];
|
|
104
|
+
case CLOUD_IDENTIFIERS.GCP_US:
|
|
105
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_US];
|
|
106
|
+
case CLOUD_IDENTIFIERS.AWS_EU:
|
|
107
|
+
case CLOUD_IDENTIFIERS.AWS_FRA:
|
|
108
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.AWS_EU];
|
|
109
|
+
case CLOUD_IDENTIFIERS.AWS_US:
|
|
110
|
+
case CLOUD_IDENTIFIERS.AWS_OHIO:
|
|
111
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.AWS_US];
|
|
112
|
+
default:
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// Given a cloud identifier, try to map it to a legacy hostname.
|
|
117
|
+
// This is for backwards compatibility.
|
|
118
|
+
const mapToLegacyIssuer = cloudIdentifier => {
|
|
119
|
+
switch (cloudIdentifier) {
|
|
120
|
+
case CLOUD_IDENTIFIERS.GCP_EU:
|
|
121
|
+
return 'https://mc-api.commercetools.com';
|
|
122
|
+
case CLOUD_IDENTIFIERS.GCP_US:
|
|
123
|
+
return 'https://mc-api.commercetools.co';
|
|
124
|
+
default:
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
// Verifies that the issuer is a valid URL.
|
|
129
|
+
const throwIfIssuerIsNotAValidUrl = issuer => {
|
|
130
|
+
try {
|
|
131
|
+
new _URL__default["default"](issuer);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new Error(`Invalid issuer URL "${issuer}". Expected a valid URL to the Merchant Center API Gateway, or a cloud identifier to one of the available cloud regions. See https://docs.commercetools.com/merchant-center-customizations/concepts/merchant-center-api#hostnames.`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
// Validates required option values.
|
|
137
|
+
const validateRequiredValues = options => {
|
|
138
|
+
if (!options.audience) {
|
|
139
|
+
throw new Error(`Missing required option "audience"`);
|
|
140
|
+
}
|
|
141
|
+
if (!options.issuer) {
|
|
142
|
+
throw new Error(`Missing required option "issuer"`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
// Attempt to parse the given issuer. If the value is a cloud identifier, it will
|
|
146
|
+
// be mapped to one of the supported values. If not, we assume the value is a valid URL.
|
|
147
|
+
const getConfiguredDefaultIssuer = options => {
|
|
148
|
+
const issuer = mapCloudIdentifierToIssuer(options.issuer);
|
|
149
|
+
if (!issuer) {
|
|
150
|
+
throwIfIssuerIsNotAValidUrl(options.issuer);
|
|
151
|
+
return options.issuer;
|
|
152
|
+
}
|
|
153
|
+
return issuer;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Construct the audience from the given option + the request path.
|
|
157
|
+
// If the request path is `/`, do not append it to the audience, otherwise
|
|
158
|
+
// the token validation might fail because of mismatching audiences.
|
|
159
|
+
const getConfiguredAudience = (options, requestPath) => {
|
|
160
|
+
// remove the trailing slash
|
|
161
|
+
const url = new _URL__default["default"](`${options.audience.replace(/\/?$/, '')}${requestPath}`);
|
|
162
|
+
switch (options.audiencePolicy) {
|
|
163
|
+
case 'forward-url-origin':
|
|
164
|
+
return url.origin;
|
|
165
|
+
default:
|
|
166
|
+
{
|
|
167
|
+
if (requestPath === '/') {
|
|
168
|
+
return url.origin;
|
|
169
|
+
}
|
|
170
|
+
return `${url.origin}${url.pathname}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const jwksClientByIssuer = new _Map__default["default"]();
|
|
175
|
+
function getJwksClientByIssuer(issuer) {
|
|
176
|
+
const client = jwksClientByIssuer.get(issuer);
|
|
177
|
+
if (client) {
|
|
178
|
+
return client;
|
|
179
|
+
}
|
|
180
|
+
const newClient = jose.createRemoteJWKSet(new _URL__default["default"](`/.well-known/jwks.json`, issuer));
|
|
181
|
+
jwksClientByIssuer.set(issuer, newClient);
|
|
182
|
+
return newClient;
|
|
183
|
+
}
|
|
184
|
+
function createSessionAuthVerifier(options) {
|
|
185
|
+
validateRequiredValues(options);
|
|
186
|
+
const configuredDefaultIssuer = getConfiguredDefaultIssuer(options);
|
|
187
|
+
|
|
188
|
+
// Returns an async HTTP handler.
|
|
189
|
+
return async request => {
|
|
190
|
+
// Get the cloud identifier header, forwarded by the `/proxy/forward-to` endpoint.
|
|
191
|
+
const cloudIdentifierHeader = getFirstHeaderValueOrThrow(request.headers, MC_API_PROXY_HEADERS.CLOUD_IDENTIFIER, `Missing "X-MC-API-Cloud-Identifier" header.`);
|
|
192
|
+
let issuer = options.inferIssuer && cloudIdentifierHeader ? mapCloudIdentifierToIssuer(cloudIdentifierHeader) ?? configuredDefaultIssuer : configuredDefaultIssuer;
|
|
193
|
+
|
|
194
|
+
// Get the `Accept-version` header, forwarded by the `/proxy/forward-to` endpoint.
|
|
195
|
+
// The version should be sent by the client making the request, to use the features of v2.
|
|
196
|
+
const proxyForwardVersion = getFirstHeaderValueOrThrow(request.headers, MC_API_PROXY_HEADERS.FORWARD_TO_VERSION, `Missing "X-MC-API-Forward-To-Version" header.`);
|
|
197
|
+
if (proxyForwardVersion === 'v1') {
|
|
198
|
+
// Fall back to legacy issuer domains
|
|
199
|
+
issuer = mapToLegacyIssuer(cloudIdentifierHeader) ?? issuer;
|
|
200
|
+
}
|
|
201
|
+
const requestUrlPath = options.getRequestUrl ? options.getRequestUrl(request) : request.originalUrl ?? request.url;
|
|
202
|
+
if (!requestUrlPath || !_startsWithInstanceProperty__default["default"](requestUrlPath).call(requestUrlPath, '/')) {
|
|
203
|
+
throw new Error(`Invalid request URI path "${requestUrlPath}". Please make sure that the "request" object has either a property "originalUrl" or "url". If not, you should implement the "getRequestUrl" function and make sure to return a valid URI path value starting with "/". More info at https://docs.commercetools.com/merchant-center-customizations/concepts/integrate-with-your-own-api#validating-the-json-web-token`);
|
|
204
|
+
}
|
|
205
|
+
const audience = getConfiguredAudience(options, requestUrlPath);
|
|
206
|
+
const authorizationHeader = request.headers['authorization'];
|
|
207
|
+
if (typeof authorizationHeader !== 'string') {
|
|
208
|
+
throw new Error(`Missing "authorization" header`);
|
|
209
|
+
}
|
|
210
|
+
const exchangeToken = authorizationHeader.replace(/^Bearer (.*)$/, '$1');
|
|
211
|
+
const jwksClient = getJwksClientByIssuer(issuer);
|
|
212
|
+
const verifiedToken = await jose.jwtVerify(exchangeToken, jwksClient, {
|
|
213
|
+
algorithms: ['RS256'],
|
|
214
|
+
audience,
|
|
215
|
+
issuer
|
|
216
|
+
});
|
|
217
|
+
writeSessionContext(request, verifiedToken.payload);
|
|
218
|
+
return;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function createSessionMiddleware(options) {
|
|
223
|
+
const sessionAuthVerifier = createSessionAuthVerifier(options);
|
|
224
|
+
return async (request, _response, next) => {
|
|
225
|
+
try {
|
|
226
|
+
await sessionAuthVerifier(request);
|
|
227
|
+
next();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
next(error);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
exports.CLOUD_IDENTIFIERS = CLOUD_IDENTIFIERS;
|
|
235
|
+
exports.MC_API_URLS = MC_API_URLS;
|
|
236
|
+
exports.createSessionAuthVerifier = createSessionAuthVerifier;
|
|
237
|
+
exports.createSessionMiddleware = createSessionMiddleware;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import _URL from '@babel/runtime-corejs3/core-js-stable/url';
|
|
2
|
+
import _Map from '@babel/runtime-corejs3/core-js-stable/map';
|
|
3
|
+
import _startsWithInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/starts-with';
|
|
4
|
+
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
5
|
+
import _slicedToArray from '@babel/runtime-corejs3/helpers/esm/slicedToArray';
|
|
6
|
+
import _findInstanceProperty from '@babel/runtime-corejs3/core-js-stable/instance/find';
|
|
7
|
+
import _Object$entries from '@babel/runtime-corejs3/core-js-stable/object/entries';
|
|
8
|
+
import _Array$isArray from '@babel/runtime-corejs3/core-js-stable/array/is-array';
|
|
9
|
+
|
|
10
|
+
const CLOUD_IDENTIFIERS = {
|
|
11
|
+
GCP_AU: 'gcp-au',
|
|
12
|
+
GCP_EU: 'gcp-eu',
|
|
13
|
+
GCP_US: 'gcp-us',
|
|
14
|
+
AWS_EU: 'aws-eu',
|
|
15
|
+
AWS_US: 'aws-us',
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated: use `AWS_EU` instead
|
|
18
|
+
*/
|
|
19
|
+
AWS_FRA: 'aws-fra',
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated: use `AWS_US` instead
|
|
22
|
+
*/
|
|
23
|
+
AWS_OHIO: 'aws-ohio'
|
|
24
|
+
};
|
|
25
|
+
const MC_API_URLS = {
|
|
26
|
+
[CLOUD_IDENTIFIERS.GCP_AU]: 'https://mc-api.australia-southeast1.gcp.commercetools.com',
|
|
27
|
+
[CLOUD_IDENTIFIERS.GCP_EU]: 'https://mc-api.europe-west1.gcp.commercetools.com',
|
|
28
|
+
[CLOUD_IDENTIFIERS.GCP_US]: 'https://mc-api.us-central1.gcp.commercetools.com',
|
|
29
|
+
[CLOUD_IDENTIFIERS.AWS_EU]: 'https://mc-api.eu-central-1.aws.commercetools.com',
|
|
30
|
+
[CLOUD_IDENTIFIERS.AWS_US]: 'https://mc-api.us-east-2.aws.commercetools.com',
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated: use `AWS_EU` instead
|
|
33
|
+
*/
|
|
34
|
+
[CLOUD_IDENTIFIERS.AWS_FRA]: 'https://mc-api.eu-central-1.aws.commercetools.com',
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated: use `AWS_US` instead
|
|
37
|
+
*/
|
|
38
|
+
[CLOUD_IDENTIFIERS.AWS_OHIO]: 'https://mc-api.us-east-2.aws.commercetools.com'
|
|
39
|
+
};
|
|
40
|
+
const MC_API_PROXY_HEADERS = {
|
|
41
|
+
FORWARD_TO_VERSION: 'x-mc-api-forward-to-version',
|
|
42
|
+
CLOUD_IDENTIFIER: 'x-mc-api-cloud-identifier'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getHeaderByCaseInsensitiveKey = (headers, headerKey) => {
|
|
46
|
+
var _context;
|
|
47
|
+
const matchingHeader = _findInstanceProperty(_context = _Object$entries(headers)).call(_context, _ref => {
|
|
48
|
+
let _ref2 = _slicedToArray(_ref, 1),
|
|
49
|
+
key = _ref2[0];
|
|
50
|
+
return headerKey.toLowerCase() === key.toLowerCase();
|
|
51
|
+
});
|
|
52
|
+
if (matchingHeader && matchingHeader.length > 0) {
|
|
53
|
+
const _matchingHeader = _slicedToArray(matchingHeader, 2),
|
|
54
|
+
headerValue = _matchingHeader[1];
|
|
55
|
+
return _Array$isArray(headerValue) ? headerValue[0] : headerValue;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
59
|
+
const getFirstHeaderValueOrThrow = (headers, headerKey, errorMessage) => {
|
|
60
|
+
const headerValue = getHeaderByCaseInsensitiveKey(headers, headerKey);
|
|
61
|
+
if (!headerValue) {
|
|
62
|
+
throw new Error(errorMessage);
|
|
63
|
+
}
|
|
64
|
+
return headerValue;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Assign a session object to the request object.
|
|
68
|
+
const writeSessionContext = (request, verifiedToken) => {
|
|
69
|
+
const publicClaimForProjectKey = `${verifiedToken.iss}/claims/project_key`;
|
|
70
|
+
const publicClaimForUserPermissionsKey = `${verifiedToken.iss}/claims/user_permissions`;
|
|
71
|
+
request.session = {
|
|
72
|
+
userId: verifiedToken.sub,
|
|
73
|
+
projectKey: verifiedToken[publicClaimForProjectKey]
|
|
74
|
+
};
|
|
75
|
+
const userPermissions = verifiedToken[publicClaimForUserPermissionsKey];
|
|
76
|
+
if (Boolean(userPermissions?.length)) {
|
|
77
|
+
request.session.userPermissions = userPermissions;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Given a cloud identifier, try to map it to one of the supported
|
|
82
|
+
// environments and return the MC API URL for that environment.
|
|
83
|
+
// The URL points to the new hostnames.
|
|
84
|
+
// https://docs.commercetools.com/merchant-center-customizations/concepts/merchant-center-api#hostnames
|
|
85
|
+
const mapCloudIdentifierToIssuer = issuer => {
|
|
86
|
+
switch (issuer) {
|
|
87
|
+
case CLOUD_IDENTIFIERS.GCP_AU:
|
|
88
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_AU];
|
|
89
|
+
case CLOUD_IDENTIFIERS.GCP_EU:
|
|
90
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_EU];
|
|
91
|
+
case CLOUD_IDENTIFIERS.GCP_US:
|
|
92
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.GCP_US];
|
|
93
|
+
case CLOUD_IDENTIFIERS.AWS_EU:
|
|
94
|
+
case CLOUD_IDENTIFIERS.AWS_FRA:
|
|
95
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.AWS_EU];
|
|
96
|
+
case CLOUD_IDENTIFIERS.AWS_US:
|
|
97
|
+
case CLOUD_IDENTIFIERS.AWS_OHIO:
|
|
98
|
+
return MC_API_URLS[CLOUD_IDENTIFIERS.AWS_US];
|
|
99
|
+
default:
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Given a cloud identifier, try to map it to a legacy hostname.
|
|
104
|
+
// This is for backwards compatibility.
|
|
105
|
+
const mapToLegacyIssuer = cloudIdentifier => {
|
|
106
|
+
switch (cloudIdentifier) {
|
|
107
|
+
case CLOUD_IDENTIFIERS.GCP_EU:
|
|
108
|
+
return 'https://mc-api.commercetools.com';
|
|
109
|
+
case CLOUD_IDENTIFIERS.GCP_US:
|
|
110
|
+
return 'https://mc-api.commercetools.co';
|
|
111
|
+
default:
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// Verifies that the issuer is a valid URL.
|
|
116
|
+
const throwIfIssuerIsNotAValidUrl = issuer => {
|
|
117
|
+
try {
|
|
118
|
+
new _URL(issuer);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`Invalid issuer URL "${issuer}". Expected a valid URL to the Merchant Center API Gateway, or a cloud identifier to one of the available cloud regions. See https://docs.commercetools.com/merchant-center-customizations/concepts/merchant-center-api#hostnames.`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
// Validates required option values.
|
|
124
|
+
const validateRequiredValues = options => {
|
|
125
|
+
if (!options.audience) {
|
|
126
|
+
throw new Error(`Missing required option "audience"`);
|
|
127
|
+
}
|
|
128
|
+
if (!options.issuer) {
|
|
129
|
+
throw new Error(`Missing required option "issuer"`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
// Attempt to parse the given issuer. If the value is a cloud identifier, it will
|
|
133
|
+
// be mapped to one of the supported values. If not, we assume the value is a valid URL.
|
|
134
|
+
const getConfiguredDefaultIssuer = options => {
|
|
135
|
+
const issuer = mapCloudIdentifierToIssuer(options.issuer);
|
|
136
|
+
if (!issuer) {
|
|
137
|
+
throwIfIssuerIsNotAValidUrl(options.issuer);
|
|
138
|
+
return options.issuer;
|
|
139
|
+
}
|
|
140
|
+
return issuer;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Construct the audience from the given option + the request path.
|
|
144
|
+
// If the request path is `/`, do not append it to the audience, otherwise
|
|
145
|
+
// the token validation might fail because of mismatching audiences.
|
|
146
|
+
const getConfiguredAudience = (options, requestPath) => {
|
|
147
|
+
// remove the trailing slash
|
|
148
|
+
const url = new _URL(`${options.audience.replace(/\/?$/, '')}${requestPath}`);
|
|
149
|
+
switch (options.audiencePolicy) {
|
|
150
|
+
case 'forward-url-origin':
|
|
151
|
+
return url.origin;
|
|
152
|
+
default:
|
|
153
|
+
{
|
|
154
|
+
if (requestPath === '/') {
|
|
155
|
+
return url.origin;
|
|
156
|
+
}
|
|
157
|
+
return `${url.origin}${url.pathname}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const jwksClientByIssuer = new _Map();
|
|
162
|
+
function getJwksClientByIssuer(issuer) {
|
|
163
|
+
const client = jwksClientByIssuer.get(issuer);
|
|
164
|
+
if (client) {
|
|
165
|
+
return client;
|
|
166
|
+
}
|
|
167
|
+
const newClient = createRemoteJWKSet(new _URL(`/.well-known/jwks.json`, issuer));
|
|
168
|
+
jwksClientByIssuer.set(issuer, newClient);
|
|
169
|
+
return newClient;
|
|
170
|
+
}
|
|
171
|
+
function createSessionAuthVerifier(options) {
|
|
172
|
+
validateRequiredValues(options);
|
|
173
|
+
const configuredDefaultIssuer = getConfiguredDefaultIssuer(options);
|
|
174
|
+
|
|
175
|
+
// Returns an async HTTP handler.
|
|
176
|
+
return async request => {
|
|
177
|
+
// Get the cloud identifier header, forwarded by the `/proxy/forward-to` endpoint.
|
|
178
|
+
const cloudIdentifierHeader = getFirstHeaderValueOrThrow(request.headers, MC_API_PROXY_HEADERS.CLOUD_IDENTIFIER, `Missing "X-MC-API-Cloud-Identifier" header.`);
|
|
179
|
+
let issuer = options.inferIssuer && cloudIdentifierHeader ? mapCloudIdentifierToIssuer(cloudIdentifierHeader) ?? configuredDefaultIssuer : configuredDefaultIssuer;
|
|
180
|
+
|
|
181
|
+
// Get the `Accept-version` header, forwarded by the `/proxy/forward-to` endpoint.
|
|
182
|
+
// The version should be sent by the client making the request, to use the features of v2.
|
|
183
|
+
const proxyForwardVersion = getFirstHeaderValueOrThrow(request.headers, MC_API_PROXY_HEADERS.FORWARD_TO_VERSION, `Missing "X-MC-API-Forward-To-Version" header.`);
|
|
184
|
+
if (proxyForwardVersion === 'v1') {
|
|
185
|
+
// Fall back to legacy issuer domains
|
|
186
|
+
issuer = mapToLegacyIssuer(cloudIdentifierHeader) ?? issuer;
|
|
187
|
+
}
|
|
188
|
+
const requestUrlPath = options.getRequestUrl ? options.getRequestUrl(request) : request.originalUrl ?? request.url;
|
|
189
|
+
if (!requestUrlPath || !_startsWithInstanceProperty(requestUrlPath).call(requestUrlPath, '/')) {
|
|
190
|
+
throw new Error(`Invalid request URI path "${requestUrlPath}". Please make sure that the "request" object has either a property "originalUrl" or "url". If not, you should implement the "getRequestUrl" function and make sure to return a valid URI path value starting with "/". More info at https://docs.commercetools.com/merchant-center-customizations/concepts/integrate-with-your-own-api#validating-the-json-web-token`);
|
|
191
|
+
}
|
|
192
|
+
const audience = getConfiguredAudience(options, requestUrlPath);
|
|
193
|
+
const authorizationHeader = request.headers['authorization'];
|
|
194
|
+
if (typeof authorizationHeader !== 'string') {
|
|
195
|
+
throw new Error(`Missing "authorization" header`);
|
|
196
|
+
}
|
|
197
|
+
const exchangeToken = authorizationHeader.replace(/^Bearer (.*)$/, '$1');
|
|
198
|
+
const jwksClient = getJwksClientByIssuer(issuer);
|
|
199
|
+
const verifiedToken = await jwtVerify(exchangeToken, jwksClient, {
|
|
200
|
+
algorithms: ['RS256'],
|
|
201
|
+
audience,
|
|
202
|
+
issuer
|
|
203
|
+
});
|
|
204
|
+
writeSessionContext(request, verifiedToken.payload);
|
|
205
|
+
return;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function createSessionMiddleware(options) {
|
|
210
|
+
const sessionAuthVerifier = createSessionAuthVerifier(options);
|
|
211
|
+
return async (request, _response, next) => {
|
|
212
|
+
try {
|
|
213
|
+
await sessionAuthVerifier(request);
|
|
214
|
+
next();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
next(error);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { CLOUD_IDENTIFIERS, MC_API_URLS, createSessionAuthVerifier, createSessionMiddleware };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TSessionMiddlewareOptions, TSession, TBaseRequest } from "./types.js";
|
|
2
|
+
export type TDecodedJWT = {
|
|
3
|
+
sub: string;
|
|
4
|
+
iss: string;
|
|
5
|
+
[property: string]: string | string[];
|
|
6
|
+
};
|
|
7
|
+
declare const writeSessionContext: <Request extends TBaseRequest>(request: Request & {
|
|
8
|
+
session?: TSession;
|
|
9
|
+
}, verifiedToken: TDecodedJWT) => void;
|
|
10
|
+
export declare const getConfiguredAudience: <Request extends TBaseRequest>(options: TSessionMiddlewareOptions<Request>, requestPath: string) => string;
|
|
11
|
+
declare function createSessionAuthVerifier<Request extends TBaseRequest>(options: TSessionMiddlewareOptions<Request>): (request: Request) => Promise<void>;
|
|
12
|
+
export { createSessionAuthVerifier, writeSessionContext };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const CLOUD_IDENTIFIERS: {
|
|
2
|
+
readonly GCP_AU: "gcp-au";
|
|
3
|
+
readonly GCP_EU: "gcp-eu";
|
|
4
|
+
readonly GCP_US: "gcp-us";
|
|
5
|
+
readonly AWS_EU: "aws-eu";
|
|
6
|
+
readonly AWS_US: "aws-us";
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated: use `AWS_EU` instead
|
|
9
|
+
*/
|
|
10
|
+
readonly AWS_FRA: "aws-fra";
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated: use `AWS_US` instead
|
|
13
|
+
*/
|
|
14
|
+
readonly AWS_OHIO: "aws-ohio";
|
|
15
|
+
};
|
|
16
|
+
export declare const MC_API_URLS: {
|
|
17
|
+
readonly "gcp-au": "https://mc-api.australia-southeast1.gcp.commercetools.com";
|
|
18
|
+
readonly "gcp-eu": "https://mc-api.europe-west1.gcp.commercetools.com";
|
|
19
|
+
readonly "gcp-us": "https://mc-api.us-central1.gcp.commercetools.com";
|
|
20
|
+
readonly "aws-eu": "https://mc-api.eu-central-1.aws.commercetools.com";
|
|
21
|
+
readonly "aws-us": "https://mc-api.us-east-2.aws.commercetools.com";
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated: use `AWS_EU` instead
|
|
24
|
+
*/
|
|
25
|
+
readonly "aws-fra": "https://mc-api.eu-central-1.aws.commercetools.com";
|
|
26
|
+
/**
|
|
27
|
+
* @deprecated: use `AWS_US` instead
|
|
28
|
+
*/
|
|
29
|
+
readonly "aws-ohio": "https://mc-api.us-east-2.aws.commercetools.com";
|
|
30
|
+
};
|
|
31
|
+
export declare const MC_API_PROXY_HEADERS: {
|
|
32
|
+
readonly FORWARD_TO_VERSION: "x-mc-api-forward-to-version";
|
|
33
|
+
readonly CLOUD_IDENTIFIER: "x-mc-api-cloud-identifier";
|
|
34
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Response, NextFunction } from 'express';
|
|
2
|
+
import type { TBaseRequest, TSessionMiddlewareOptions } from "../types.js";
|
|
3
|
+
declare function createSessionMiddleware<Request extends TBaseRequest>(options: TSessionMiddlewareOptions<Request>): (request: Request, _response: Response, next: NextFunction) => Promise<void>;
|
|
4
|
+
export default createSessionMiddleware;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CLOUD_IDENTIFIERS } from "./constants.js";
|
|
2
|
+
export type TAudience = string;
|
|
3
|
+
export type TIssuer = string;
|
|
4
|
+
export type TCloudIdentifier = (typeof CLOUD_IDENTIFIERS)[keyof typeof CLOUD_IDENTIFIERS];
|
|
5
|
+
export type TAudiencePolicy = 'forward-url-full-path' | 'forward-url-origin';
|
|
6
|
+
export interface TBaseRequest {
|
|
7
|
+
headers: Record<string, string | string[] | undefined>;
|
|
8
|
+
url?: string;
|
|
9
|
+
originalUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export type TSessionMiddlewareOptions<Request extends TBaseRequest> = {
|
|
12
|
+
/**
|
|
13
|
+
* The public-facing URL used to connect to the server / serverless function.
|
|
14
|
+
* The value should only contain the origin URL (protocol, hostname, port),
|
|
15
|
+
* the request path is inferred from the incoming request.
|
|
16
|
+
*/
|
|
17
|
+
audience: TAudience;
|
|
18
|
+
/**
|
|
19
|
+
* How the audience value should be exchanged between the server and the client.
|
|
20
|
+
* By default it uses the full URL (origin + pathname).
|
|
21
|
+
*/
|
|
22
|
+
audiencePolicy?: TAudiencePolicy;
|
|
23
|
+
/**
|
|
24
|
+
* The cloud identifier (see `CLOUD_IDENTIFIERS`) that maps to the MC API URL
|
|
25
|
+
* of the related cloud region or the MC API URL.
|
|
26
|
+
*/
|
|
27
|
+
issuer: TCloudIdentifier | TIssuer;
|
|
28
|
+
/**
|
|
29
|
+
* Determines whether the issuer should be inferred from the custom request
|
|
30
|
+
* HTTP header `x-mc-api-cloud-identifier` which is sent by the MC API when
|
|
31
|
+
* forwarding the request.
|
|
32
|
+
* This might be useful in case the server is used in multiple regions.
|
|
33
|
+
*/
|
|
34
|
+
inferIssuer?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Options for the `jwksRsa.expressJwtSecret`
|
|
37
|
+
*
|
|
38
|
+
* @deprecated
|
|
39
|
+
*/
|
|
40
|
+
jwks?: unknown;
|
|
41
|
+
/**
|
|
42
|
+
* By default we assume that the `request` is a Node.js-like object having either
|
|
43
|
+
* an `originalUrl` or `url` properties.
|
|
44
|
+
* If that's not the case (for example in AWS Lambda functions) you need to correctly
|
|
45
|
+
* map the URL (URI path + query string) of the `request`.
|
|
46
|
+
*/
|
|
47
|
+
getRequestUrl?: (request: Request) => string;
|
|
48
|
+
};
|
|
49
|
+
export type TSession = {
|
|
50
|
+
userId: string;
|
|
51
|
+
projectKey: string;
|
|
52
|
+
userPermissions?: string[];
|
|
53
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@commercetools-backend/express",
|
|
3
|
+
"version": "0.0.0-CRAFT-1791-20251006162610",
|
|
4
|
+
"description": "Zero-config HTTP server as Express.js to facilitate development",
|
|
5
|
+
"bugs": "https://github.com/commercetools/merchant-center-application-kit/issues",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/commercetools/merchant-center-application-kit.git",
|
|
9
|
+
"directory": "packages-backend/express"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.commercetools.com/merchant-center-customizations",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"javascript",
|
|
14
|
+
"nodejs",
|
|
15
|
+
"express",
|
|
16
|
+
"http",
|
|
17
|
+
"server",
|
|
18
|
+
"toolkit"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"main": "dist/commercetools-backend-express.cjs.js",
|
|
25
|
+
"module": "dist/commercetools-backend-express.esm.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"package.json",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@babel/runtime": "^7.22.15",
|
|
34
|
+
"@babel/runtime-corejs3": "^7.22.15",
|
|
35
|
+
"@types/express": "^4.17.17",
|
|
36
|
+
"@types/node": "^22.13.2",
|
|
37
|
+
"express": "^4.21.2",
|
|
38
|
+
"jose": "^5.10.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@tsconfig/node22": "^22.0.0",
|
|
42
|
+
"msw": "^1.3.5",
|
|
43
|
+
"typescript": "^5.2.2"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": "18.x || 20.x || >=22.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|