@adobe/helix-html-pipeline 6.1.0 → 6.1.2
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 +14 -0
- package/package.json +4 -4
- package/src/PipelineState.d.ts +5 -0
- package/src/auth-pipe.js +57 -0
- package/src/html-pipe.js +0 -15
- package/src/index.js +1 -1
- package/src/utils/auth.js +76 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [6.1.2](https://github.com/adobe/helix-html-pipeline/compare/v6.1.1...v6.1.2) (2024-01-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* shorten auth state ([#493](https://github.com/adobe/helix-html-pipeline/issues/493)) ([72b3ede](https://github.com/adobe/helix-html-pipeline/commit/72b3ede01a0aee189aa66277407950489b718a2d))
|
|
7
|
+
|
|
8
|
+
## [6.1.1](https://github.com/adobe/helix-html-pipeline/compare/v6.1.0...v6.1.1) (2024-01-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* handle /.auth path ([#490](https://github.com/adobe/helix-html-pipeline/issues/490)) ([ce2d761](https://github.com/adobe/helix-html-pipeline/commit/ce2d7617b6abd138a5f58ffb2788e698396191fc))
|
|
14
|
+
|
|
1
15
|
# [6.1.0](https://github.com/adobe/helix-html-pipeline/compare/v6.0.1...v6.1.0) (2024-01-13)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-html-pipeline",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2",
|
|
4
4
|
"description": "Helix HTML Pipeline",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"hast-util-to-string": "3.0.0",
|
|
55
55
|
"hastscript": "8.0.0",
|
|
56
56
|
"jose": "5.2.0",
|
|
57
|
-
"mdast-util-to-hast": "13.0
|
|
57
|
+
"mdast-util-to-hast": "13.1.0",
|
|
58
58
|
"mdast-util-to-string": "4.0.0",
|
|
59
59
|
"mime": "4.0.1",
|
|
60
60
|
"rehype-format": "5.0.0",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@semantic-release/changelog": "6.0.3",
|
|
76
76
|
"@semantic-release/git": "10.0.1",
|
|
77
77
|
"@semantic-release/npm": "11.0.2",
|
|
78
|
-
"c8": "9.
|
|
78
|
+
"c8": "9.1.0",
|
|
79
79
|
"eslint": "8.56.0",
|
|
80
80
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
|
81
81
|
"eslint-plugin-header": "3.1.1",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"esmock": "2.6.0",
|
|
84
84
|
"husky": "8.0.3",
|
|
85
85
|
"js-yaml": "4.1.0",
|
|
86
|
-
"jsdom": "23.
|
|
86
|
+
"jsdom": "23.2.0",
|
|
87
87
|
"junit-report-builder": "3.1.0",
|
|
88
88
|
"lint-staged": "15.2.0",
|
|
89
89
|
"mocha": "10.2.0",
|
package/src/PipelineState.d.ts
CHANGED
|
@@ -141,5 +141,10 @@ declare class PipelineState {
|
|
|
141
141
|
* the custom live host if configured via config.cdn.live.host
|
|
142
142
|
*/
|
|
143
143
|
liveHost: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* used for development server to include RSO information in the auth state
|
|
147
|
+
*/
|
|
148
|
+
authIncludeRSO: boolean;
|
|
144
149
|
}
|
|
145
150
|
|
package/src/auth-pipe.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2021 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
|
|
13
|
+
import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
|
|
14
|
+
import { PipelineResponse } from './PipelineResponse.js';
|
|
15
|
+
import { validateAuthState, getAuthInfo } from './utils/auth.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runs the auth pipeline that handles the token exchange. this is separated from the main pipeline
|
|
19
|
+
* since it doesn't need the configuration.
|
|
20
|
+
*
|
|
21
|
+
* @param {PipelineState} state
|
|
22
|
+
* @param {PipelineRequest} req
|
|
23
|
+
* @returns {PipelineResponse}
|
|
24
|
+
*/
|
|
25
|
+
export async function authPipe(state, req) {
|
|
26
|
+
const { log } = state;
|
|
27
|
+
|
|
28
|
+
/** @type PipelineResponse */
|
|
29
|
+
const res = new PipelineResponse('', {
|
|
30
|
+
headers: {
|
|
31
|
+
'content-type': 'text/html; charset=utf-8',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await validateAuthState(state, req);
|
|
37
|
+
const authInfo = await getAuthInfo(state, req);
|
|
38
|
+
await authInfo.exchangeToken(state, req, res);
|
|
39
|
+
/* c8 ignore next */
|
|
40
|
+
const level = res.status >= 500 ? 'error' : 'info';
|
|
41
|
+
log[level](`pipeline status: ${res.status} ${res.error}`);
|
|
42
|
+
res.headers.set('x-error', cleanupHeaderValue(res.error));
|
|
43
|
+
if (res.status < 500) {
|
|
44
|
+
await setCustomResponseHeaders(state, req, res);
|
|
45
|
+
}
|
|
46
|
+
return res;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return new PipelineResponse('', {
|
|
49
|
+
status: 401,
|
|
50
|
+
headers: {
|
|
51
|
+
'cache-control': 'no-store, private, must-revalidate',
|
|
52
|
+
'content-type': 'text/html; charset=utf-8',
|
|
53
|
+
'x-error': e.message,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/html-pipe.js
CHANGED
|
@@ -36,7 +36,6 @@ import tohtml from './steps/stringify-response.js';
|
|
|
36
36
|
import { PipelineStatusError } from './PipelineStatusError.js';
|
|
37
37
|
import { PipelineResponse } from './PipelineResponse.js';
|
|
38
38
|
import { validatePathInfo } from './utils/path.js';
|
|
39
|
-
import { getAuthInfo } from './utils/auth.js';
|
|
40
39
|
import fetchMappedMetadata from './steps/fetch-mapped-metadata.js';
|
|
41
40
|
|
|
42
41
|
/**
|
|
@@ -104,20 +103,6 @@ export async function htmlPipe(state, req) {
|
|
|
104
103
|
},
|
|
105
104
|
});
|
|
106
105
|
|
|
107
|
-
// check if `.auth` route to validate and exchange token
|
|
108
|
-
if (state.partition === '.auth') {
|
|
109
|
-
const authInfo = await getAuthInfo(state, req);
|
|
110
|
-
await authInfo.exchangeToken(state, req, res);
|
|
111
|
-
/* c8 ignore next */
|
|
112
|
-
const level = res.status >= 500 ? 'error' : 'info';
|
|
113
|
-
log[level](`pipeline status: ${res.status} ${res.error}`);
|
|
114
|
-
res.headers.set('x-error', cleanupHeaderValue(res.error));
|
|
115
|
-
if (res.status < 500) {
|
|
116
|
-
await setCustomResponseHeaders(state, req, res);
|
|
117
|
-
}
|
|
118
|
-
return res;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
106
|
try {
|
|
122
107
|
await initConfig(state, req, res);
|
|
123
108
|
|
package/src/index.js
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './html-pipe.js';
|
|
13
13
|
export * from './json-pipe.js';
|
|
14
|
+
export * from './auth-pipe.js';
|
|
14
15
|
export * from './options-pipe.js';
|
|
15
16
|
export * from './PipelineContent.js';
|
|
16
17
|
export * from './PipelineRequest.js';
|
|
17
18
|
export * from './PipelineResponse.js';
|
|
18
19
|
export * from './PipelineState.js';
|
|
19
20
|
export * from './PipelineStatusError.js';
|
|
20
|
-
export { validateAuthState } from './utils/auth.js';
|
package/src/utils/auth.js
CHANGED
|
@@ -31,6 +31,16 @@ let ADMIN_KEY_PAIR = null;
|
|
|
31
31
|
export class AccessDeniedError extends Error {
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async function getAdminKeyPair(state) {
|
|
35
|
+
if (!ADMIN_KEY_PAIR) {
|
|
36
|
+
ADMIN_KEY_PAIR = {
|
|
37
|
+
privateKey: await importJWK(JSON.parse(state.env.HLX_ADMIN_IDP_PRIVATE_KEY), 'RS256'),
|
|
38
|
+
publicKey: JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return ADMIN_KEY_PAIR;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* Signs the given JWT with the admin private key and returns the token.
|
|
36
46
|
* @param {PipelineState} state
|
|
@@ -38,13 +48,7 @@ export class AccessDeniedError extends Error {
|
|
|
38
48
|
* @returns {Promise<string>}
|
|
39
49
|
*/
|
|
40
50
|
async function signJWT(state, jwt) {
|
|
41
|
-
|
|
42
|
-
ADMIN_KEY_PAIR = {
|
|
43
|
-
privateKey: await importJWK(JSON.parse(state.env.HLX_ADMIN_IDP_PRIVATE_KEY), 'RS256'),
|
|
44
|
-
publicKey: JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY),
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const { privateKey, publicKey } = ADMIN_KEY_PAIR;
|
|
51
|
+
const { privateKey, publicKey } = await getAdminKeyPair(state);
|
|
48
52
|
return jwt
|
|
49
53
|
.setProtectedHeader({
|
|
50
54
|
alg: 'RS256',
|
|
@@ -55,6 +59,23 @@ async function signJWT(state, jwt) {
|
|
|
55
59
|
.sign(privateKey);
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Creates the auth state JWT for redirecting back to the initial page
|
|
64
|
+
* @param {PipelineState} state
|
|
65
|
+
* @param {SignJWT} jwt
|
|
66
|
+
* @returns {Promise<string>}
|
|
67
|
+
*/
|
|
68
|
+
async function createStateJWT(state, jwt) {
|
|
69
|
+
const { privateKey, publicKey } = await getAdminKeyPair(state);
|
|
70
|
+
return jwt
|
|
71
|
+
.setProtectedHeader({
|
|
72
|
+
alg: 'RS256',
|
|
73
|
+
kid: publicKey.kid,
|
|
74
|
+
})
|
|
75
|
+
.setIssuer(publicKey.issuer)
|
|
76
|
+
.sign(privateKey);
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
/**
|
|
59
80
|
* Verifies and decodes the given jwt using the admin public key
|
|
60
81
|
* @param {PipelineState} state
|
|
@@ -75,6 +96,23 @@ async function verifyJwt(state, jwt, lenient = false) {
|
|
|
75
96
|
return payload;
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Verifies and decodes the given state jwt using the admin public key
|
|
101
|
+
* @param {PipelineState} state
|
|
102
|
+
* @param {string} jwt
|
|
103
|
+
* @returns {Promise<JWTPayload>}
|
|
104
|
+
*/
|
|
105
|
+
async function verifyStateJwt(state, jwt) {
|
|
106
|
+
const publicKey = JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY);
|
|
107
|
+
const jwks = createLocalJWKSet({
|
|
108
|
+
keys: [publicKey],
|
|
109
|
+
});
|
|
110
|
+
const { payload } = await jwtVerify(jwt, jwks, {
|
|
111
|
+
issuer: publicKey.issuer,
|
|
112
|
+
});
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
|
|
78
116
|
/**
|
|
79
117
|
* Decodes the given id_token for the given idp. if `lenient` is `true`, the clock tolerance
|
|
80
118
|
* is set to 1 week. this allows to extract some profile information that can be used as login_hint.
|
|
@@ -228,7 +266,7 @@ export class AuthInfo {
|
|
|
228
266
|
}
|
|
229
267
|
|
|
230
268
|
// determine the location of 'this' document based on the xfh header. so that logins to
|
|
231
|
-
// .page stay on .page. etc. but fallback to the
|
|
269
|
+
// .page stay on .page. etc. but fallback to the production host if not set
|
|
232
270
|
const { host, proto } = getRequestHostAndProto(state, req);
|
|
233
271
|
if (!host) {
|
|
234
272
|
log.error('[auth] unable to create login redirect: no xfh or config.host.');
|
|
@@ -236,18 +274,21 @@ export class AuthInfo {
|
|
|
236
274
|
return;
|
|
237
275
|
}
|
|
238
276
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
277
|
+
// create the token state, so stat we know where to redirect back after the token exchange
|
|
278
|
+
const payload = {
|
|
279
|
+
url: `${proto}://${host}${state.info.path}`,
|
|
280
|
+
};
|
|
281
|
+
// this is for the development server to remember the org, site, ref and partition
|
|
282
|
+
// normally, this is not needed as the host is used to determine that information
|
|
283
|
+
if (state.authIncludeRSO) {
|
|
284
|
+
payload.org = state.org;
|
|
285
|
+
payload.site = state.site;
|
|
286
|
+
payload.ref = state.ref;
|
|
287
|
+
payload.partition = state.partition;
|
|
288
|
+
}
|
|
289
|
+
const tokenState = await createStateJWT(state, new SignJWT(payload));
|
|
250
290
|
|
|
291
|
+
const url = new URL(idp.discovery.authorization_endpoint);
|
|
251
292
|
url.searchParams.append('client_id', clientId);
|
|
252
293
|
url.searchParams.append('response_type', 'code');
|
|
253
294
|
url.searchParams.append('scope', idp.scope);
|
|
@@ -370,8 +411,8 @@ export class AuthInfo {
|
|
|
370
411
|
}
|
|
371
412
|
}
|
|
372
413
|
|
|
373
|
-
export async function validateAuthState(
|
|
374
|
-
const { log } =
|
|
414
|
+
export async function validateAuthState(state, req) {
|
|
415
|
+
const { log } = state;
|
|
375
416
|
// use request headers if present
|
|
376
417
|
if (req.headers.get('x-hlx-auth-state')) {
|
|
377
418
|
log.info('[auth] override params.state from header.');
|
|
@@ -389,19 +430,24 @@ export async function validateAuthState(ctx, req) {
|
|
|
389
430
|
|
|
390
431
|
try {
|
|
391
432
|
req.params.rawState = req.params.state;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
433
|
+
const payload = await verifyStateJwt(state, req.params.state);
|
|
434
|
+
const url = new URL(payload.url);
|
|
435
|
+
req.params.state = {
|
|
436
|
+
requestPath: url.pathname,
|
|
437
|
+
requestHost: url.host,
|
|
438
|
+
requestProto: url.protocol.replace(/:$/, ''),
|
|
439
|
+
};
|
|
440
|
+
// for development server
|
|
441
|
+
if (payload.org && payload.site && payload.ref && payload.partition) {
|
|
442
|
+
state.org = payload.org;
|
|
443
|
+
state.site = payload.site;
|
|
444
|
+
state.ref = payload.ref;
|
|
445
|
+
state.partition = payload.partition;
|
|
446
|
+
}
|
|
395
447
|
} catch (e) {
|
|
396
448
|
log.warn(`[auth] error decoding state parameter: invalid state: ${e.message}`);
|
|
397
449
|
throw new Error('invalid state parameter.');
|
|
398
450
|
}
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
ref: req.params.state.ref,
|
|
402
|
-
site: req.params.state.site,
|
|
403
|
-
org: req.params.state.org,
|
|
404
|
-
};
|
|
405
451
|
}
|
|
406
452
|
|
|
407
453
|
/**
|