@adobe/helix-html-pipeline 6.10.3 → 6.12.0

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,19 @@
1
+ # [6.12.0](https://github.com/adobe/helix-html-pipeline/compare/v6.11.0...v6.12.0) (2024-05-22)
2
+
3
+
4
+ ### Features
5
+
6
+ * add contentbusid surrogate key for pipeline responses depending on content resources ([e821162](https://github.com/adobe/helix-html-pipeline/commit/e821162b9afc649fbb53ef550d51fd61b4ccc346))
7
+ * add ref--repo--owner_code surrogate key for pipeline responses depending on codebus resources ([797aad2](https://github.com/adobe/helix-html-pipeline/commit/797aad2a1b34aa7eb95fc9ef57a65b60afed2c76))
8
+ * add ref--repo-owner_code for all code resources ([edaa518](https://github.com/adobe/helix-html-pipeline/commit/edaa518db81259d4c247c6cc8445277f41a3ccfb))
9
+
10
+ # [6.11.0](https://github.com/adobe/helix-html-pipeline/compare/v6.10.3...v6.11.0) (2024-05-13)
11
+
12
+
13
+ ### Features
14
+
15
+ * remove auth ([#598](https://github.com/adobe/helix-html-pipeline/issues/598)) ([2eed6f4](https://github.com/adobe/helix-html-pipeline/commit/2eed6f4852fc4b74582ab953be79b77c17e3029b))
16
+
1
17
  ## [6.10.3](https://github.com/adobe/helix-html-pipeline/compare/v6.10.2...v6.10.3) (2024-05-09)
2
18
 
3
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "6.10.3",
3
+ "version": "6.12.0",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -47,14 +47,12 @@
47
47
  "@adobe/helix-shared-utils": "3.0.2",
48
48
  "@adobe/mdast-util-gridtables": "4.0.4",
49
49
  "@adobe/remark-gridtables": "3.0.4",
50
- "cookie": "0.6.0",
51
50
  "github-slugger": "2.0.0",
52
- "hast-util-raw": "9.0.2",
51
+ "hast-util-raw": "9.0.3",
53
52
  "hast-util-select": "6.0.2",
54
53
  "hast-util-to-html": "9.0.1",
55
54
  "hast-util-to-string": "3.0.0",
56
55
  "hastscript": "9.0.0",
57
- "jose": "5.2.4",
58
56
  "lodash.escape": "4.0.1",
59
57
  "mdast-util-to-hast": "13.1.0",
60
58
  "mdast-util-to-string": "4.0.0",
@@ -76,7 +74,7 @@
76
74
  "@markedjs/html-differ": "4.0.2",
77
75
  "@semantic-release/changelog": "6.0.3",
78
76
  "@semantic-release/git": "10.0.1",
79
- "@semantic-release/npm": "12.0.0",
77
+ "@semantic-release/npm": "12.0.1",
80
78
  "c8": "9.1.0",
81
79
  "eslint": "8.57.0",
82
80
  "eslint-import-resolver-exports": "1.0.0-beta.5",
@@ -91,7 +89,7 @@
91
89
  "mocha": "10.4.0",
92
90
  "mocha-multi-reporters": "1.5.1",
93
91
  "mocha-suppress-logs": "0.5.1",
94
- "semantic-release": "23.0.8"
92
+ "semantic-release": "23.1.1"
95
93
  },
96
94
  "lint-staged": {
97
95
  "*.js": "eslint",
@@ -9,7 +9,7 @@
9
9
  * OF ANY KIND; either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer, AuthEnvLoader } from "./index";
12
+ import {PathInfo, S3Loader, PipelineTimer } from "./index";
13
13
  import {PipelineContent} from "./PipelineContent";
14
14
  import {PipelineSiteConfig} from "./site-config";
15
15
 
@@ -21,21 +21,9 @@ declare enum PipelineType {
21
21
 
22
22
  type Fetch = (url: string|Request, options?: RequestOptions) => Promise<Response>;
23
23
 
24
- declare interface AccessConfig {
25
- allow:(string|string[]);
26
-
27
- apiKeyId:(string|string[]);
28
-
29
- require: {
30
- repository:(string|string[]);
31
- };
32
- }
33
-
34
24
  declare interface PipelineOptions {
35
25
  log: Console;
36
26
  s3Loader: S3Loader;
37
- messageDispatcher: FormsMessageDispatcher;
38
- authEnvLoader: AuthEnvLoader;
39
27
  config: PipelineSiteConfig;
40
28
  fetch: Fetch;
41
29
  ref: string;
@@ -55,16 +43,8 @@ declare class PipelineState {
55
43
  content: PipelineContent;
56
44
  contentBusId: string;
57
45
  s3Loader: S3Loader;
58
- messageDispatcher: FormsMessageDispatcher;
59
- authEnvLoader: AuthEnvLoader;
60
46
  fetch: Fetch;
61
47
 
62
- /**
63
- * Returns the external link representation for authentication related redirects and cookies.
64
- * This is only used for local testing and is an identity operation in production.
65
- */
66
- createExternalLocation(value:string): string;
67
-
68
48
  /**
69
49
  * Content bus partition
70
50
  * @example 'live'
@@ -122,11 +102,6 @@ declare class PipelineState {
122
102
  */
123
103
  type: PipelineType;
124
104
 
125
- /**
126
- * Authentication information
127
- */
128
- authInfo?: AuthInfo;
129
-
130
105
  /**
131
106
  * the production host
132
107
  */
@@ -142,9 +117,5 @@ declare class PipelineState {
142
117
  */
143
118
  liveHost: string;
144
119
 
145
- /**
146
- * used for development server to include RSO information in the auth state
147
- */
148
- authIncludeRSO: boolean;
149
120
  }
150
121
 
@@ -40,8 +40,6 @@ export class PipelineState {
40
40
  metadata: Modifiers.EMPTY,
41
41
  headers: Modifiers.EMPTY,
42
42
  s3Loader: opts.s3Loader,
43
- messageDispatcher: opts.messageDispatcher,
44
- authEnvLoader: opts.authEnvLoader ?? { load: () => {} },
45
43
  fetch: opts.fetch,
46
44
  timer: opts.timer,
47
45
  type: 'html',
@@ -52,9 +50,4 @@ export class PipelineState {
52
50
  }
53
51
  }
54
52
  }
55
-
56
- // eslint-disable-next-line class-methods-use-this
57
- createExternalLocation(value) {
58
- return value;
59
- }
60
53
  }
package/src/html-pipe.js CHANGED
@@ -10,7 +10,6 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
  import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
- import { authenticate } from './steps/authenticate.js';
14
13
  import addHeadingIds from './steps/add-heading-ids.js';
15
14
  import createPageBlocks from './steps/create-page-blocks.js';
16
15
  import createPictures from './steps/create-pictures.js';
@@ -135,11 +134,6 @@ export async function htmlPipe(state, req) {
135
134
  fetchMappedMetadata(state),
136
135
  ]);
137
136
 
138
- // await requireProject(state, req, res);
139
- if (res.error !== 401) {
140
- await authenticate(state, req, res);
141
- }
142
-
143
137
  if (res.error) {
144
138
  // if content loading produced an error, we're done.
145
139
  const level = res.status >= 500 ? 'error' : 'info';
package/src/index.d.ts CHANGED
@@ -88,29 +88,11 @@ export declare interface S3Loader {
88
88
  headObject(bucketId, key): Promise<PipelineResponse>;
89
89
  }
90
90
 
91
- export declare interface AuthEnvLoader {
92
-
93
- /**
94
- * loads (secret) parameters needed for authentication. The parameters are added to the
95
- * `state.env` object.
96
- * @return {Promise<void>}
97
- */
98
- load(state:PipelineState):Promise<void>;
99
- }
100
-
101
91
  export declare interface DispatchMessageResponse {
102
92
  messageId:string,
103
93
  requestId:string,
104
94
  }
105
95
 
106
- export declare interface FormsMessageDispatcher {
107
- /**
108
- * Dispatches the message to the forms queue
109
- * @param {object} message
110
- */
111
- dispatch(message:object): Promise<DispatchMessageResponse>;
112
- }
113
-
114
96
  /**
115
97
  * Timer
116
98
  */
package/src/index.js CHANGED
@@ -11,7 +11,6 @@
11
11
  */
12
12
  export * from './html-pipe.js';
13
13
  export * from './json-pipe.js';
14
- export * from './auth-pipe.js';
15
14
  export * from './options-pipe.js';
16
15
  export * from './robots-pipe.js';
17
16
  export * from './sitemap-pipe.js';
package/src/json-pipe.js CHANGED
@@ -15,7 +15,6 @@ import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
15
15
  import { PipelineResponse } from './PipelineResponse.js';
16
16
  import jsonFilter from './utils/json-filter.js';
17
17
  import { extractLastModified, updateLastModified } from './utils/last-modified.js';
18
- import { authenticate } from './steps/authenticate.js';
19
18
  import { getPathInfo } from './utils/path.js';
20
19
  import { PipelineStatusError } from './PipelineStatusError.js';
21
20
 
@@ -62,7 +61,15 @@ async function fetchJsonContent(state, req, res) {
62
61
  res.body = '';
63
62
  res.headers.delete('content-type');
64
63
  res.headers.set('location', redirectLocation);
65
- res.headers.set('x-surrogate-key', await computeSurrogateKey(`${contentBusId}${info.path}`));
64
+ const keys = [];
65
+ if (state.content.sourceBus === 'content') {
66
+ keys.push(await computeSurrogateKey(`${contentBusId}${info.path}`));
67
+ keys.push(contentBusId);
68
+ } else {
69
+ keys.push(`${ref}--${repo}--${owner}_code`);
70
+ keys.push(await computeSurrogateKey(`${ref}--${repo}--${owner}${info.path}`));
71
+ }
72
+ res.headers.set('x-surrogate-key', keys.join(' '));
66
73
  res.error = 'moved';
67
74
  return;
68
75
  }
@@ -93,8 +100,12 @@ async function computeSurrogateKeys(state) {
93
100
  if (state.info.path === '/config.json') {
94
101
  keys.push(await computeSurrogateKey(`${state.site}--${state.org}_config.json`));
95
102
  }
96
- keys.push(pathKey.replace(/\//g, '_')); // TODO: remove
97
103
  keys.push(await computeSurrogateKey(pathKey));
104
+ if (state.content?.sourceBus === 'content') {
105
+ keys.push(state.contentBusId);
106
+ } else {
107
+ keys.push(`${state.ref}--${state.repo}--${state.owner}_code`);
108
+ }
98
109
  return keys;
99
110
  }
100
111
 
@@ -150,8 +161,6 @@ export async function jsonPipe(state, req) {
150
161
 
151
162
  state.timer?.update('json-metadata-fetch');
152
163
 
153
- await authenticate(state, req, res);
154
-
155
164
  if (res.status === 404 && state.info.path === '/config.json' && state.config.public) {
156
165
  // special handling for public config
157
166
  const publicConfig = {
@@ -120,7 +120,6 @@ async function computeSurrogateKeys(state) {
120
120
 
121
121
  const pathKey = `${state.ref}--${state.repo}--${state.owner}${state.info.path}`;
122
122
  keys.push(await computeSurrogateKey(`${state.site}--${state.org}_config.json`));
123
- keys.push(pathKey.replace(/\//g, '_')); // TODO: remove
124
123
  keys.push(await computeSurrogateKey(pathKey));
125
124
  return keys;
126
125
  }
@@ -38,5 +38,5 @@ export default async function fetch404(state, req, res) {
38
38
 
39
39
  // set 404 keys in any case
40
40
  const pathKey = await getPathKey(state);
41
- res.headers.set('x-surrogate-key', `${pathKey} ${ref}--${repo}--${owner}_404`);
41
+ res.headers.set('x-surrogate-key', `${pathKey} ${ref}--${repo}--${owner}_404 ${ref}--${repo}--${owner}_code`);
42
42
  }
@@ -41,7 +41,15 @@ export default async function fetchContent(state, req, res) {
41
41
  redirectLocation += '.plain.html';
42
42
  }
43
43
  res.headers.set('location', redirectLocation);
44
- res.headers.set('x-surrogate-key', await computeSurrogateKey(`${contentBusId}${info.path}`));
44
+ const keys = [];
45
+ if (isCode) {
46
+ keys.push(await computeSurrogateKey(`${ref}--${repo}--${owner}${info.path}`));
47
+ keys.push(`${ref}--${repo}--${owner}_code`);
48
+ } else {
49
+ keys.push(await computeSurrogateKey(`${contentBusId}${info.path}`));
50
+ keys.push(contentBusId);
51
+ }
52
+ res.headers.set('x-surrogate-key', keys.join(' '));
45
53
  res.error = 'moved';
46
54
  return;
47
55
  }
@@ -49,6 +49,7 @@ export default async function setXSurrogateKeyHeader(state, req, res) {
49
49
  hash,
50
50
  `${contentBusId}_metadata`,
51
51
  `${ref}--${repo}--${owner}_head`,
52
+ contentBusId,
52
53
  ];
53
54
 
54
55
  // for folder-mapped resources, we also need to include the surrogate key of the mapped metadata
package/src/auth-pipe.js DELETED
@@ -1,46 +0,0 @@
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 { PipelineResponse } from './PipelineResponse.js';
14
- import { validateAuthState, getRequestHostAndProto, AuthInfo } from './utils/auth.js';
15
- import { clearAuthCookie } from './utils/auth-cookie.js';
16
- import idpMicrosoft from './utils/idp-configs/microsoft.js';
17
-
18
- /**
19
- * Runs the auth pipeline that handles the token exchange. this is separated from the main pipeline
20
- * since it doesn't need the configuration (yet).
21
- *
22
- * @param {PipelineState} state
23
- * @param {PipelineRequest} req
24
- * @returns {PipelineResponse}
25
- */
26
- export async function authPipe(ctx, req) {
27
- try {
28
- await validateAuthState(ctx, req);
29
- const authInfo = AuthInfo
30
- .Default()
31
- // todo: select idp from config
32
- .withIdp(idpMicrosoft);
33
- return await authInfo.exchangeToken(ctx, req);
34
- } catch (e) {
35
- const { proto } = getRequestHostAndProto(ctx, req);
36
- return new PipelineResponse('', {
37
- status: 401,
38
- headers: {
39
- 'cache-control': 'no-store, private, must-revalidate',
40
- 'content-type': 'text/html; charset=utf-8',
41
- 'x-error': cleanupHeaderValue(e.message),
42
- 'set-cookie': clearAuthCookie(proto === 'https'),
43
- },
44
- });
45
- }
46
- }
@@ -1,129 +0,0 @@
1
- /*
2
- * Copyright 2022 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 { getAuthInfo, makeAuthError } from '../utils/auth.js';
13
-
14
- /**
15
- * Checks if the given email is allowed.
16
- * @param {string} email
17
- * @param {string[]} allows
18
- * @returns {boolean}
19
- */
20
- export function isAllowed(email = '', allows = []) {
21
- /** @type string[] */
22
- const [, domain] = email.split('@');
23
- if (!domain) {
24
- return false;
25
- }
26
- const wild = `*@${domain}`;
27
- return allows.findIndex((a) => a === email || a === wild) >= 0;
28
- }
29
-
30
- /**
31
- * Handles authentication
32
- * @type PipelineStep
33
- * @param {PipelineState} state
34
- * @param {PipelineRequest} req
35
- * @param {PipelineResponse} res
36
- * @returns {Promise<void>}
37
- */
38
- export async function authenticate(state, req, res) {
39
- // get partition relative auth info
40
- const access = state.config.access?.[state.partition] || {
41
- allow: [],
42
- apiKeyId: [],
43
- };
44
-
45
- // if not protected, do nothing
46
- if (!access.allow?.length) {
47
- return;
48
- }
49
-
50
- // get auth info
51
- const authInfo = await getAuthInfo(state, req);
52
-
53
- // if not authenticated, redirect to login screen
54
- if (!authInfo.authenticated) {
55
- // send 401 for plain requests
56
- if (state.info.selector || state.type !== 'html') {
57
- state.log.warn('[auth] unauthorized. redirect to login only for extension less html.');
58
- makeAuthError(state, req, res, 'unauthorized');
59
- return;
60
- }
61
- await authInfo.redirectToLogin(state, req, res);
62
- return;
63
- }
64
-
65
- const { sub, jti, email } = authInfo.profile;
66
-
67
- // validate subject, if present
68
- if (sub) {
69
- const [owner, repo] = sub.split('/');
70
- if (owner !== state.owner || (repo !== '*' && repo !== state.repo)) {
71
- state.log.warn(`[auth] invalid subject ${sub}: does not match ${state.owner}/${state.repo}`);
72
- makeAuthError(state, req, res, 'invalid-subject');
73
- return;
74
- }
75
- }
76
-
77
- // validate jti
78
- if (jti) {
79
- if (access.apiKeyId.indexOf(jti) < 0) {
80
- state.log.warn(`[auth] invalid jti ${jti}: does not match configured id ${access.apiKeyId}`);
81
- makeAuthError(state, req, res, 'invalid-jti');
82
- }
83
- }
84
-
85
- // check profile is allowed
86
- if (!isAllowed(email, access.allow)) {
87
- state.log.warn(`[auth] profile not allowed for ${access.allow}`);
88
- makeAuthError(state, req, res, 'forbidden', 403);
89
- }
90
- }
91
-
92
- /**
93
- * Checks if the given owner repo is allowed
94
- * @param {string} owner
95
- * @param {string} repo
96
- * @param {string[]} allows
97
- * @returns {boolean}
98
- */
99
- export function isOwnerRepoAllowed(owner, repo, allows = []) {
100
- if (allows.length === 0) {
101
- return true;
102
- }
103
- return allows
104
- .map((ownerRepo) => ownerRepo.split('/'))
105
- .findIndex(([o, r]) => owner === o && (repo === r || r === '*')) >= 0;
106
- }
107
-
108
- /**
109
- * Checks if the
110
- * @type PipelineStep
111
- * @param {PipelineState} state
112
- * @param {PipelineRequest} req
113
- * @param {PipelineResponse} res
114
- * @returns {Promise<void>}
115
- */
116
- // export async function requireProject(state, req, res) {
117
- // // if not restricted, do nothing
118
- // const ownerRepo = state.config?.access?.require?.repository;
119
- // if (!ownerRepo) {
120
- // return;
121
- // }
122
- // const ownerRepos = Array.isArray(ownerRepo) ? ownerRepo : [ownerRepo];
123
- // const { log, owner, repo } = state;
124
- // if (!isOwnerRepoAllowed(owner, repo, ownerRepos)) {
125
- // log.warn(`${owner}/${repo} not allowed for ${ownerRepos}`);
126
- // res.status = 403;
127
- // res.error = 'forbidden.';
128
- // }
129
- // }
@@ -1,41 +0,0 @@
1
- /*
2
- * Copyright 2022 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 { parse, serialize } from 'cookie';
13
-
14
- export function clearAuthCookie(secure) {
15
- return serialize('hlx-auth-token', '', {
16
- path: '/',
17
- httpOnly: true,
18
- secure,
19
- expires: new Date(0),
20
- sameSite: secure ? 'none' : 'lax',
21
- });
22
- }
23
-
24
- export function setAuthCookie(idToken, secure) {
25
- return serialize('hlx-auth-token', idToken, {
26
- path: '/',
27
- httpOnly: true,
28
- secure,
29
- sameSite: secure ? 'none' : 'lax',
30
- });
31
- }
32
-
33
- export function getAuthCookie(req) {
34
- // add cookies if not already present
35
- if (!req.cookies) {
36
- const hdr = req.headers.get('cookie');
37
- // eslint-disable-next-line no-param-reassign
38
- req.cookies = hdr ? parse(hdr) : {};
39
- }
40
- return req.cookies['hlx-auth-token'] || '';
41
- }
@@ -1,86 +0,0 @@
1
- /*
2
- * Copyright 2022 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
-
13
-
14
- import {AdminContext} from "../index";
15
-
16
- /**
17
- * Path Info
18
- */
19
- export declare interface AccessDeniedError extends Error {}
20
-
21
- export declare interface OAuthClientConfig {
22
- clientID: string;
23
- clientSecret: string;
24
- }
25
-
26
- export declare interface IDPConfig {
27
- name:string;
28
- scope:string;
29
- mountType:string;
30
- client(state: PipelineState):OAuthClientConfig;
31
- validateIssuer?(issuer: string): boolean;
32
- discoveryUrl:string;
33
- loginPrompt:string;
34
- discovery:any;
35
- routes:AuthRoutes;
36
- }
37
-
38
- export declare interface UserProfile {
39
- email:string;
40
-
41
- iss:string;
42
-
43
- aud:string;
44
-
45
- sub: string;
46
-
47
- jti: string;
48
- }
49
-
50
- export declare class AuthInfo {
51
- /**
52
- * Flag indicating of the request is authenticated
53
- */
54
- authenticated:boolean;
55
-
56
- profile?:UserProfile;
57
-
58
- expired?:boolean;
59
-
60
- loginHint?:string;
61
-
62
- idp?:IDPConfig;
63
-
64
- /**
65
- * Flag indicating that the auth cookie is invalid.
66
- */
67
- cookieInvalid?:boolean;
68
-
69
- /**
70
- * Sets a redirect (302) response to the IDPs login endpoint
71
- *
72
- * @param {PipelineState} state
73
- * @param {PipelineRequest} req
74
- * @param {PipelineResponse} res
75
- */
76
- async redirectToLogin(state, req, res);
77
-
78
- /**
79
- * Performs a token exchange from the code flow and redirects to the root page
80
- *
81
- * @param {PipelineState} state
82
- * @param {PipelineRequest} req
83
- * @param {PipelineResponse} res
84
- */
85
- async exchangeToken(state, req, res);
86
- }
package/src/utils/auth.js DELETED
@@ -1,452 +0,0 @@
1
- /*
2
- * Copyright 2022 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
- // eslint-disable-next-line max-classes-per-file
13
- import {
14
- createLocalJWKSet,
15
- decodeJwt,
16
- jwtVerify,
17
- SignJWT,
18
- importJWK,
19
- } from 'jose';
20
- import { clearAuthCookie, getAuthCookie, setAuthCookie } from './auth-cookie.js';
21
-
22
- import idpMicrosoft from './idp-configs/microsoft.js';
23
-
24
- // eslint-disable-next-line import/no-unresolved
25
- import cryptoImpl from '#crypto';
26
- import { PipelineResponse } from '../PipelineResponse.js';
27
-
28
- const AUTH_REDIRECT_URL = 'https://login.aem.page/.auth';
29
-
30
- let ADMIN_KEY_PAIR = null;
31
-
32
- export class AccessDeniedError extends Error {
33
- }
34
-
35
- async function getAdminKeyPair(state) {
36
- if (!ADMIN_KEY_PAIR) {
37
- ADMIN_KEY_PAIR = {
38
- privateKey: await importJWK(JSON.parse(state.env.HLX_ADMIN_IDP_PRIVATE_KEY), 'RS256'),
39
- publicKey: JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY),
40
- };
41
- }
42
- return ADMIN_KEY_PAIR;
43
- }
44
-
45
- /**
46
- * Signs the given JWT with the admin private key and returns the token.
47
- * @param {PipelineState} state
48
- * @param {SignJWT} jwt
49
- * @returns {Promise<string>}
50
- */
51
- async function signJWT(state, jwt) {
52
- const { privateKey, publicKey } = await getAdminKeyPair(state);
53
- return jwt
54
- .setProtectedHeader({
55
- alg: 'RS256',
56
- kid: publicKey.kid,
57
- })
58
- .setAudience(state.env.HLX_SITE_APP_AZURE_CLIENT_ID)
59
- .setIssuer(publicKey.issuer)
60
- .sign(privateKey);
61
- }
62
-
63
- /**
64
- * Verifies and decodes the given jwt using the admin public key
65
- * @param {PipelineState} state
66
- * @param {string} jwt
67
- * @param {boolean} lenient
68
- * @returns {Promise<JWTPayload>}
69
- */
70
- async function verifyJwt(state, jwt, lenient = false) {
71
- const publicKey = JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY);
72
- const jwks = createLocalJWKSet({
73
- keys: [publicKey],
74
- });
75
- const { payload } = await jwtVerify(jwt, jwks, {
76
- audience: state.env.HLX_SITE_APP_AZURE_CLIENT_ID,
77
- issuer: publicKey.issuer,
78
- clockTolerance: lenient ? 7 * 24 * 60 * 60 : 0,
79
- });
80
- return payload;
81
- }
82
-
83
- /**
84
- * Decodes the given id_token for the given idp. if `lenient` is `true`, the clock tolerance
85
- * is set to 1 week. this allows to extract some profile information that can be used as login_hint.
86
- * @param {PipelineState} state
87
- * @param {string} idToken
88
- * @param {boolean} lenient
89
- * @returns {Promise<JWTPayload>}
90
- */
91
- export async function decodeIdToken(state, idToken, lenient = false) {
92
- const { log } = state;
93
- const payload = await verifyJwt(state, idToken, lenient);
94
-
95
- // delete from information not needed in the profile
96
- ['azp', 'at_hash', 'nonce', 'aio', 'c_hash'].forEach((prop) => delete payload[prop]);
97
-
98
- // compute ttl
99
- payload.ttl = payload.exp - Math.floor(Date.now() / 1000);
100
-
101
- log.info(`[auth] decoded id_token${lenient ? ' (lenient)' : ''} from ${payload.iss} and validated payload.`);
102
- return payload;
103
- }
104
-
105
- /**
106
- * Returns the host of the request; falls back to the configured `host`.
107
- * Note that this is different from the `state.prodHost` calculation in `init-config`,
108
- * as this prefers the xfh over the config.
109
- *
110
- * @param {PipelineState} state
111
- * @param {PipelineRequest} req
112
- * @returns {{proto: (*|string), host: string}} the request host and protocol.
113
- */
114
- export function getRequestHostAndProto(state, req) {
115
- // determine the location of 'this' document based on the xfh header. so that logins to
116
- // .page stay on .page. etc. but fallback to the config.host if non set
117
- const xfh = req.headers.get('x-forwarded-host');
118
- let host = xfh;
119
- if (host) {
120
- host = host.split(',')[0].trim();
121
- }
122
- if (!host) {
123
- host = state.prodHost;
124
- }
125
- // fastly overrides the x-forwarded-proto, so we use x-forwarded-scheme
126
- const proto = req.headers.get('x-forwarded-scheme') || req.headers.get('x-forwarded-proto') || 'https';
127
- state.log.info(`request host is: ${host} (${proto}) (xfh=${xfh})`);
128
- return {
129
- host,
130
- proto,
131
- };
132
- }
133
-
134
- /**
135
- * sets the auth error on the response and clears the cookie.
136
- * @param state
137
- * @param req
138
- * @param res
139
- * @param error
140
- * @param status
141
- */
142
- export function makeAuthError(state, req, res, error, status = 401) {
143
- const { proto } = getRequestHostAndProto(state, req);
144
- res.status = status;
145
- res.error = error;
146
- res.headers.set('set-cookie', clearAuthCookie(proto === 'https'));
147
- res.headers.set('x-error', error);
148
- }
149
-
150
- /**
151
- * AuthInfo class
152
- */
153
- export class AuthInfo {
154
- /**
155
- * AuthInfo constructor
156
- * @constructor
157
- */
158
- constructor() {
159
- Object.assign(this, {
160
- authenticated: false,
161
- idp: null,
162
- profile: null,
163
- loginHint: null,
164
- expired: false,
165
- idToken: null,
166
- cookieInvalid: false,
167
- });
168
- }
169
-
170
- /**
171
- * Creates the default AuthInfo that is not authenticated.
172
- * @returns {AuthInfo}
173
- */
174
- static Default() {
175
- return new AuthInfo()
176
- .withAuthenticated(false);
177
- }
178
-
179
- withAuthenticated(value) {
180
- this.authenticated = value;
181
- return this;
182
- }
183
-
184
- withProfile(profile) {
185
- this.profile = profile;
186
- return this;
187
- }
188
-
189
- withLoginHint(value) {
190
- this.loginHint = value;
191
- return this;
192
- }
193
-
194
- withIdp(value) {
195
- this.idp = value;
196
- return this;
197
- }
198
-
199
- withExpired(value) {
200
- this.expired = value;
201
- return this;
202
- }
203
-
204
- withCookieInvalid(value) {
205
- this.cookieInvalid = value;
206
- return this;
207
- }
208
-
209
- withIdToken(value) {
210
- this.idToken = value;
211
- return this;
212
- }
213
-
214
- /**
215
- * Sets a redirect (302) response to the IDPs login endpoint
216
- *
217
- * @param {PipelineState} state
218
- * @param {PipelineRequest} req
219
- * @param {PipelineResponse} res
220
- * @param {IDPConfig} idp IDP config
221
- */
222
- async redirectToLogin(state, req, res) {
223
- const { log } = state;
224
- const { idp } = this;
225
-
226
- await state.authEnvLoader.load(state);
227
- const { clientId, clientSecret } = idp.client(state);
228
- if (!clientId || !clientSecret) {
229
- log.error('[auth] unable to create login redirect: missing client_id or client_secret');
230
- res.status = 401;
231
- res.error = 'invalid auth config.';
232
- return;
233
- }
234
-
235
- // determine the location of 'this' document based on the xfh header. so that logins to
236
- // .page stay on .page. etc. but fallback to the config.host if non set
237
- const { host, proto } = getRequestHostAndProto(state, req);
238
- if (!host) {
239
- log.error('[auth] unable to create login redirect: no xfh or config.host.');
240
- makeAuthError(state, req, res, 'no host information.');
241
- return;
242
- }
243
-
244
- // create the token state, so stat we know where to redirect back after the token exchange
245
- const payload = {
246
- url: state.createExternalLocation(`${proto}://${host}${state.info.path}`),
247
- };
248
- const tokenState = await signJWT(state, new SignJWT(payload));
249
-
250
- const url = new URL(idp.discovery.authorization_endpoint);
251
- url.searchParams.append('client_id', clientId);
252
- url.searchParams.append('response_type', 'code');
253
- url.searchParams.append('scope', idp.scope);
254
- url.searchParams.append('nonce', cryptoImpl.randomUUID());
255
- url.searchParams.append('state', tokenState);
256
- url.searchParams.append('redirect_uri', AUTH_REDIRECT_URL);
257
- url.searchParams.append('prompt', 'select_account');
258
-
259
- log.info('[auth] redirecting to login page', url.href);
260
- res.status = 302;
261
- res.body = '';
262
- res.headers.set('location', url.href);
263
- res.headers.set('set-cookie', clearAuthCookie(proto === 'https'));
264
- res.headers.set('cache-control', 'no-store, private, must-revalidate');
265
- res.error = 'moved';
266
- }
267
-
268
- /**
269
- * Performs a token exchange from the code flow and redirects to the root page
270
- *
271
- * @param {universalContext} ctx
272
- * @param {PipelineRequest} req
273
- * @return {PipelineResponse} res
274
- * @throws {Error} if the token exchange fails
275
- */
276
- async exchangeToken(ctx, req) {
277
- const { log } = ctx;
278
- const { idp } = this;
279
-
280
- const { code } = req.params;
281
- if (!code) {
282
- log.warn('[auth] code exchange failed: code parameter missing.');
283
- throw new Error('code exchange failed.');
284
- }
285
-
286
- const { clientId, clientSecret } = idp.client(ctx);
287
- const url = new URL(idp.discovery.token_endpoint);
288
- const body = {
289
- client_id: clientId,
290
- client_secret: clientSecret,
291
- code,
292
- grant_type: 'authorization_code',
293
- redirect_uri: AUTH_REDIRECT_URL,
294
- };
295
- const { fetch } = ctx;
296
- const ret = await fetch(url.href, {
297
- method: 'POST',
298
- body: new URLSearchParams(body).toString(),
299
- headers: {
300
- 'content-type': 'application/x-www-form-urlencoded',
301
- },
302
- });
303
- if (!ret.ok) {
304
- log.warn(`[auth] code exchange failed: ${ret.status}`, await ret.text());
305
- throw new Error('code exchange failed.');
306
- }
307
-
308
- const tokenResponse = await ret.json();
309
- const { id_token: idToken } = tokenResponse;
310
- let payload;
311
- try {
312
- payload = decodeJwt(idToken);
313
- } catch (e) {
314
- log.warn(`[auth] id token from ${idp.name} is invalid: ${e.message}`);
315
- throw new Error('id token invalid.');
316
- }
317
-
318
- const email = payload.email || payload.preferred_username;
319
- if (!email) {
320
- log.warn(`[auth] id token from ${idp.name} is missing email or preferred_username`);
321
- throw new Error('id token invalid.');
322
- }
323
-
324
- // create new token
325
- const jwt = new SignJWT({
326
- email,
327
- name: payload.name,
328
- })
329
- .setIssuedAt()
330
- .setExpirationTime('12 hours');
331
- const authToken = await signJWT(ctx, jwt);
332
-
333
- // redirect to original page
334
- const location = req.params.state.url;
335
- log.info('[auth] redirecting to original page with hlx-auth-token cookie:', location);
336
- return new PipelineResponse(`please go to <a href="${location}">${location}</a>`, {
337
- status: 302,
338
- headers: {
339
- 'content-type': 'text/html; charset=utf-8',
340
- 'set-cookie': setAuthCookie(authToken, location.startsWith('https://')),
341
- 'cache-control': 'no-store, private, must-revalidate',
342
- location,
343
- },
344
- });
345
- }
346
- }
347
-
348
- /**
349
- * Validates the auth state and code either with from query parameter or request header.
350
- * @param {UniversalContext} ctx
351
- * @param {PipelineRequest} req
352
- * @returns {Promise<void>}
353
- */
354
- export async function validateAuthState(ctx, req) {
355
- const { log } = ctx;
356
- // use request headers if present
357
- if (req.headers.get('x-hlx-auth-state')) {
358
- log.info('[auth] override params.state from header.');
359
- req.params.state = req.headers.get('x-hlx-auth-state');
360
- }
361
- if (req.headers.get('x-hlx-auth-code')) {
362
- log.info('[auth] override params.code from header.');
363
- req.params.code = req.headers.get('x-hlx-auth-code');
364
- }
365
-
366
- if (!req.params.state) {
367
- log.warn('[auth] unable to exchange token: no state.');
368
- throw new Error('missing state parameter.');
369
- }
370
-
371
- try {
372
- const payload = await verifyJwt(ctx, req.params.state);
373
- req.params.state = {
374
- url: payload.url,
375
- };
376
- } catch (e) {
377
- log.warn(`[auth] error decoding state parameter: invalid state: ${e.message}`);
378
- throw new Error('invalid state parameter.');
379
- }
380
- }
381
-
382
- /**
383
- * Extracts the authentication info from the cookie or 'authorization' header.
384
- * Returns {@code null} if missing or invalid.
385
- *
386
- * @param {PipelineState} state
387
- * @param {PipelineRequest} req
388
- * @returns {Promise<AuthInfo>} the authentication info or null if the request is not authenticated
389
- */
390
- async function getAuthInfoFromCookieOrHeader(state, req) {
391
- const { log } = state;
392
- let idToken = getAuthCookie(req);
393
- if (!idToken) {
394
- log.debug('no auth cookie');
395
- const [marker, value] = (req.headers.get('authorization') || '').split(' ');
396
- if (marker.toLowerCase() === 'token' && value) {
397
- idToken = value.trim();
398
- } else {
399
- log.debug('no auth header');
400
- }
401
- }
402
- if (idToken) {
403
- try {
404
- return AuthInfo.Default()
405
- .withProfile(await decodeIdToken(state, idToken))
406
- .withAuthenticated(true)
407
- .withIdToken(idToken);
408
- } catch (e) {
409
- if (e.code === 'ERR_JWT_EXPIRED') {
410
- try {
411
- const profile = await decodeIdToken(state, idToken, true);
412
- log.warn(`[auth] decoding the id_token failed: ${e.message}, using expired token as hint.`);
413
- return AuthInfo.Default()
414
- .withExpired(true)
415
- .withLoginHint(profile.email);
416
- } catch {
417
- // ignore
418
- }
419
- }
420
- // wrong token
421
- log.warn(`[auth] decoding the id_token failed: ${e.message}.`);
422
- return AuthInfo.Default().withCookieInvalid(true);
423
- }
424
- }
425
- log.debug('no id_token');
426
- return null;
427
- }
428
-
429
- /**
430
- * Computes the authentication info.
431
- * @param {PipelineState} state
432
- * @param {PipelineRequest} req
433
- * @returns {Promise<AuthInfo>} the authentication info or null if the request is not authenticated
434
- */
435
- export async function getAuthInfo(state, req) {
436
- const { log } = state;
437
- const auth = await getAuthInfoFromCookieOrHeader(state, req);
438
- if (auth) {
439
- if (auth.authenticated) {
440
- log.info(`[auth] id-token valid: iss=${auth.profile.iss}`);
441
- }
442
- if (!auth.idp) {
443
- // todo: select idp from config
444
- auth.withIdp(idpMicrosoft);
445
- }
446
- return auth;
447
- }
448
- return AuthInfo
449
- .Default()
450
- // todo: select idp from config
451
- .withIdp(idpMicrosoft);
452
- }
@@ -1,35 +0,0 @@
1
- /*
2
- * Copyright 2022 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
- export default {
13
- name: 'microsoft',
14
- mountType: 'onedrive',
15
- client: (state) => ({
16
- clientId: state.env.HLX_SITE_APP_AZURE_CLIENT_ID,
17
- clientSecret: state.env.HLX_SITE_APP_AZURE_CLIENT_SECRET,
18
- }),
19
- scope: 'openid profile email',
20
- // validateIssuer: (iss) => iss.startsWith('https://login.microsoftonline.com/'),
21
- discoveryUrl: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
22
- // todo: fetch from discovery document
23
- discovery: {
24
- issuer: 'https://login.microsoftonline.com/{tenantid}/v2.0',
25
- request_uri_parameter_supported: false,
26
- token_endpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
27
- userinfo_endpoint: 'https://graph.microsoft.com/oidc/userinfo',
28
- authorization_endpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
29
- device_authorization_endpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
30
- http_logout_supported: true,
31
- frontchannel_logout_supported: true,
32
- end_session_endpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/logout',
33
- jwks_uri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys',
34
- },
35
- };