@adobe/helix-html-pipeline 3.0.1 → 3.1.1

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,24 @@
1
+ ## [3.1.1](https://github.com/adobe/helix-html-pipeline/compare/v3.1.0...v3.1.1) (2022-07-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deps:** update dependency mdast-util-to-hast to v12.1.2 ([1c863bc](https://github.com/adobe/helix-html-pipeline/commit/1c863bcb06ef8af8ac3c6367121fa3a71aad38f5))
7
+
8
+ # [3.1.0](https://github.com/adobe/helix-html-pipeline/compare/v3.0.2...v3.1.0) (2022-06-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * improve authentication implementation ([#90](https://github.com/adobe/helix-html-pipeline/issues/90)) ([def347c](https://github.com/adobe/helix-html-pipeline/commit/def347cd0d4860d2804d71be6b702a6be30d6095)), closes [#85](https://github.com/adobe/helix-html-pipeline/issues/85)
14
+
15
+ ## [3.0.2](https://github.com/adobe/helix-html-pipeline/compare/v3.0.1...v3.0.2) (2022-06-16)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * make crypto.randomUUID() portable ([d40ba5a](https://github.com/adobe/helix-html-pipeline/commit/d40ba5ab67c764726d923061d4844e5adb162c86))
21
+
1
22
  ## [3.0.1](https://github.com/adobe/helix-html-pipeline/compare/v3.0.0...v3.0.1) (2022-06-14)
2
23
 
3
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -42,12 +42,12 @@
42
42
  "hast-util-to-html": "8.0.3",
43
43
  "hast-util-to-string": "2.0.0",
44
44
  "hastscript": "7.0.2",
45
- "jose": "4.8.1",
45
+ "jose": "4.8.3",
46
46
  "mdast-util-gfm-footnote": "1.0.1",
47
47
  "mdast-util-gfm-strikethrough": "1.0.1",
48
48
  "mdast-util-gfm-table": "1.0.4",
49
49
  "mdast-util-gfm-task-list-item": "1.0.1",
50
- "mdast-util-to-hast": "12.1.1",
50
+ "mdast-util-to-hast": "12.1.2",
51
51
  "mdast-util-to-string": "3.1.0",
52
52
  "micromark-extension-gfm-footnote": "1.0.4",
53
53
  "micromark-extension-gfm-strikethrough": "1.0.4",
@@ -75,16 +75,16 @@
75
75
  "@semantic-release/git": "10.0.1",
76
76
  "@semantic-release/npm": "9.0.1",
77
77
  "c8": "7.11.3",
78
- "eslint": "8.17.0",
78
+ "eslint": "8.19.0",
79
79
  "eslint-plugin-header": "3.1.1",
80
80
  "eslint-plugin-import": "2.26.0",
81
81
  "esmock": "1.7.5",
82
82
  "husky": "8.0.1",
83
83
  "js-yaml": "4.1.0",
84
84
  "jsdoc-to-markdown": "7.1.1",
85
- "jsdom": "19.0.0",
85
+ "jsdom": "20.0.0",
86
86
  "junit-report-builder": "3.0.0",
87
- "lint-staged": "13.0.1",
87
+ "lint-staged": "13.0.3",
88
88
  "mocha": "10.0.0",
89
89
  "mocha-multi-reporters": "1.5.1",
90
90
  "remark-gfm": "3.0.1",
@@ -44,11 +44,13 @@ declare interface PipelineOptions {
44
44
  path: string;
45
45
  contentBusId: string;
46
46
  timer: PipelineTimer;
47
+ env: object;
47
48
  }
48
49
 
49
50
  declare class PipelineState {
50
51
  constructor(opts: PipelineOptions);
51
52
  log: Console;
53
+ env: object;
52
54
  info: PathInfo;
53
55
  content: PipelineContent;
54
56
  contentBusId: string;
@@ -26,6 +26,7 @@ export class PipelineState {
26
26
  constructor(opts) {
27
27
  Object.assign(this, {
28
28
  log: opts.log ?? console,
29
+ env: opts.env,
29
30
  info: getPathInfo(opts.path),
30
31
  content: new PipelineContent(),
31
32
  // todo: compute content-bus id from fstab
@@ -78,6 +78,6 @@ export async function authenticate(state, req, res) {
78
78
  res.headers.set('x-hlx-auth-iss', authInfo.profile.iss);
79
79
  res.headers.set('x-hlx-auth-kid', authInfo.profile.kid);
80
80
  res.headers.set('x-hlx-auth-aud', authInfo.profile.aud);
81
- res.headers.set('x-hlx-auth-jwk', JSON.stringify(authInfo.profile.jwk));
81
+ res.headers.set('x-hlx-auth-key', authInfo.profile.pem);
82
82
  }
83
83
  }
package/src/utils/auth.js CHANGED
@@ -10,14 +10,24 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  // eslint-disable-next-line max-classes-per-file
13
- import crypto from 'crypto';
14
13
  import {
15
- createLocalJWKSet, createRemoteJWKSet, decodeJwt, jwtVerify, UnsecuredJWT,
14
+ createLocalJWKSet, createRemoteJWKSet, decodeJwt, jwtVerify, UnsecuredJWT, exportSPKI,
16
15
  } from 'jose';
17
16
  import { clearAuthCookie, getAuthCookie, setAuthCookie } from './auth-cookie.js';
18
17
 
19
18
  import idpMicrosoft from './idp-configs/microsoft.js';
20
19
 
20
+ let cryptoImpl;
21
+ import('crypto')
22
+ .then((crypto) => {
23
+ cryptoImpl = crypto;
24
+ })
25
+ /* c8 ignore next 3 */
26
+ .catch(() => {
27
+ // eslint-disable-next-line no-undef
28
+ cryptoImpl = crypto;
29
+ });
30
+
21
31
  export const IDPS = [
22
32
  idpMicrosoft,
23
33
  ];
@@ -54,16 +64,38 @@ export async function decodeIdToken(state, idp, idToken, lenient = false) {
54
64
  payload.ttl = payload.exp - Math.floor(Date.now() / 1000);
55
65
 
56
66
  // export the public key
57
- payload.jwk = key.export({
58
- type: 'pkcs1',
59
- format: 'jwk',
60
- });
67
+ payload.pem = await exportSPKI(key);
68
+ // and encode it base64 url
69
+ payload.pem = Buffer.from(payload.pem, 'utf-8').toString('base64url');
61
70
  payload.kid = protectedHeader.kid;
62
71
 
63
72
  log.info(`[auth] decoded id_token${lenient ? ' (lenient)' : ''} from ${payload.iss} and validated payload.`);
64
73
  return payload;
65
74
  }
66
75
 
76
+ /**
77
+ * Returns the host of the request; falls back to the configured `host`.
78
+ * Note that this is different from the `config.host` calculation in `fetch-config-all`,
79
+ * as this prefers the xfh over the config.
80
+ *
81
+ * @param {PipelineState} state
82
+ * @param {PipelineRequest} req
83
+ * @return {string}
84
+ */
85
+ function getRequestHost(state, req) {
86
+ // determine the location of 'this' document based on the xfh header. so that logins to
87
+ // .page stay on .page. etc. but fallback to the config.host if non set
88
+ let host = req.headers.get('x-forwarded-host');
89
+ if (host) {
90
+ host = host.split(',')[0].trim();
91
+ }
92
+ if (!host) {
93
+ host = state.config.host;
94
+ }
95
+ state.log.info(`request host is: ${host}`);
96
+ return host;
97
+ }
98
+
67
99
  /**
68
100
  * AuthInfo class
69
101
  */
@@ -150,13 +182,7 @@ export class AuthInfo {
150
182
 
151
183
  // determine the location of 'this' document based on the xfh header. so that logins to
152
184
  // .page stay on .page. etc. but fallback to the config.host if non set
153
- let host = req.headers.get('x-forwarded-host');
154
- if (host) {
155
- host = host.split(',')[0].trim();
156
- }
157
- if (!host) {
158
- host = state.config.host;
159
- }
185
+ const host = getRequestHost(state, req);
160
186
  if (!host) {
161
187
  log.error('[auth] unable to create login redirect: no xfh or config.host.');
162
188
  res.status = 401;
@@ -179,7 +205,7 @@ export class AuthInfo {
179
205
  url.searchParams.append('client_id', clientId);
180
206
  url.searchParams.append('response_type', 'code');
181
207
  url.searchParams.append('scope', idp.scope);
182
- url.searchParams.append('nonce', crypto.randomUUID());
208
+ url.searchParams.append('nonce', cryptoImpl.randomUUID());
183
209
  url.searchParams.append('state', tokenState);
184
210
  url.searchParams.append('redirect_uri', state.createExternalLocation(AUTH_REDIRECT_URL));
185
211
  url.searchParams.append('prompt', 'select_account');
@@ -214,7 +240,7 @@ export class AuthInfo {
214
240
 
215
241
  // ensure that the request is made to the target host
216
242
  if (req.params.state?.requestHost) {
217
- const host = req.headers.get('x-forwarded-host') || state.config.host;
243
+ const host = getRequestHost(state, req);
218
244
  if (host !== req.params.state.requestHost) {
219
245
  const url = new URL(`https://${req.params.state.requestHost}/.auth`);
220
246
  url.searchParams.append('state', req.params.rawState);
@@ -320,15 +346,25 @@ export function initAuthRoute(state, req, res) {
320
346
  }
321
347
 
322
348
  /**
323
- * Extracts the authentication info from the cookie. Returns {@code null} if missing or invalid.
349
+ * Extracts the authentication info from the cookie or 'authorization' header.
350
+ * Returns {@code null} if missing or invalid.
324
351
  *
325
352
  * @param {PipelineState} state
326
353
  * @param {PipelineRequest} req
327
354
  * @returns {Promise<AuthInfo>} the authentication info or null if the request is not authenticated
328
355
  */
329
- async function getAuthInfoFromCookie(state, req) {
356
+ async function getAuthInfoFromCookieOrHeader(state, req) {
330
357
  const { log } = state;
331
- const idToken = getAuthCookie(req);
358
+ let idToken = getAuthCookie(req);
359
+ if (!idToken) {
360
+ log.info('no auth cookie');
361
+ const [marker, value] = (req.headers.get('authorization') || '').split(' ');
362
+ if (marker.toLowerCase() === 'token' && value) {
363
+ idToken = value.trim();
364
+ } else {
365
+ log.info('no auth header');
366
+ }
367
+ }
332
368
  if (idToken) {
333
369
  let idp;
334
370
  try {
@@ -365,6 +401,7 @@ async function getAuthInfoFromCookie(state, req) {
365
401
  return AuthInfo.Default().withCookieInvalid(true);
366
402
  }
367
403
  }
404
+ log.info('no id_token');
368
405
  return null;
369
406
  }
370
407
 
@@ -376,7 +413,7 @@ async function getAuthInfoFromCookie(state, req) {
376
413
  */
377
414
  export async function getAuthInfo(state, req) {
378
415
  const { log } = state;
379
- const auth = await getAuthInfoFromCookie(state, req);
416
+ const auth = await getAuthInfoFromCookieOrHeader(state, req);
380
417
  if (auth) {
381
418
  if (auth.authenticated) {
382
419
  log.info(`[auth] id-token valid: iss=${auth.profile.iss}`);
@@ -12,9 +12,9 @@
12
12
  export default {
13
13
  name: 'microsoft',
14
14
  mountType: 'onedrive',
15
- client: () => ({
16
- clientId: process.env.HLX_SITE_APP_AZURE_CLIENT_ID,
17
- clientSecret: process.env.HLX_SITE_APP_AZURE_CLIENT_SECRET,
15
+ client: (state) => ({
16
+ clientId: state.env.HLX_SITE_APP_AZURE_CLIENT_ID,
17
+ clientSecret: state.env.HLX_SITE_APP_AZURE_CLIENT_SECRET,
18
18
  }),
19
19
  scope: 'openid profile email',
20
20
  validateIssuer: (iss) => iss.startsWith('https://login.microsoftonline.com/'),