@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 +16 -0
- package/package.json +4 -6
- package/src/PipelineState.d.ts +1 -30
- package/src/PipelineState.js +0 -7
- package/src/html-pipe.js +0 -6
- package/src/index.d.ts +0 -18
- package/src/index.js +0 -1
- package/src/json-pipe.js +14 -5
- package/src/robots-pipe.js +0 -1
- package/src/steps/fetch-404.js +1 -1
- package/src/steps/fetch-content.js +9 -1
- package/src/steps/set-x-surrogate-key-header.js +1 -0
- package/src/auth-pipe.js +0 -46
- package/src/steps/authenticate.js +0 -129
- package/src/utils/auth-cookie.js +0 -41
- package/src/utils/auth.d.ts +0 -86
- package/src/utils/auth.js +0 -452
- package/src/utils/idp-configs/microsoft.js +0 -35
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.
|
|
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.
|
|
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.
|
|
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.
|
|
92
|
+
"semantic-release": "23.1.1"
|
|
95
93
|
},
|
|
96
94
|
"lint-staged": {
|
|
97
95
|
"*.js": "eslint",
|
package/src/PipelineState.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
package/src/PipelineState.js
CHANGED
|
@@ -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
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
|
-
|
|
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 = {
|
package/src/robots-pipe.js
CHANGED
|
@@ -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
|
}
|
package/src/steps/fetch-404.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
// }
|
package/src/utils/auth-cookie.js
DELETED
|
@@ -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
|
-
}
|
package/src/utils/auth.d.ts
DELETED
|
@@ -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
|
-
};
|