@adobe/helix-html-pipeline 6.1.1 → 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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## [6.1.1](https://github.com/adobe/helix-html-pipeline/compare/v6.1.0...v6.1.1) (2024-01-13)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "6.1.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.2",
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.0.0",
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.1.0",
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",
@@ -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
 
@@ -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' || state.info.path === '/.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
- if (!ADMIN_KEY_PAIR) {
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 config.host if non set
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
- const url = new URL(idp.discovery.authorization_endpoint);
240
-
241
- const tokenState = await signJWT(state, new SignJWT({
242
- ref: state.ref,
243
- org: state.org,
244
- site: state.site,
245
- // this is our own login redirect, i.e. the current document
246
- requestPath: state.info.path,
247
- requestHost: host,
248
- requestProto: proto,
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(ctx, req) {
374
- const { log } = ctx;
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
- req.params.state = await verifyJwt(ctx, req.params.state);
393
- delete req.params.state.aud;
394
- delete req.params.state.iss;
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
  /**