@adobe/helix-html-pipeline 3.11.22 → 4.0.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 +10 -5
- package/package.json +2 -2
- package/src/PipelineState.d.ts +5 -1
- package/src/PipelineState.js +1 -0
- package/src/html-pipe.js +2 -2
- package/src/index.d.ts +10 -0
- package/src/steps/authenticate.js +33 -21
- package/src/utils/auth.d.ts +8 -3
- package/src/utils/auth.js +110 -69
- package/src/utils/idp-configs/microsoft.js +1 -1
- package/src/utils/idp-configs/admin.js +0 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [4.0.1](https://github.com/adobe/helix-html-pipeline/compare/v4.0.0...v4.0.1) (2023-07-18)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
* ignore urns, hrefs, code blocks for rewrite-icons ([
|
|
6
|
+
* ignore urns, hrefs, code blocks for rewrite-icons ([d61e23a](https://github.com/adobe/helix-html-pipeline/commit/d61e23a6a64ea764b935dd6a4d396c409d00bcb9)), closes [#348](https://github.com/adobe/helix-html-pipeline/issues/348)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# [4.0.0](https://github.com/adobe/helix-html-pipeline/compare/v3.11.20...v4.0.0) (2023-07-11)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* use global auth ([dfc0e06](https://github.com/adobe/helix-html-pipeline/commit/dfc0e060239ae5fb14473904568de1ac041a8271)), closes [#324](https://github.com/adobe/helix-html-pipeline/issues/324) [#285](https://github.com/adobe/helix-html-pipeline/issues/285) [#286](https://github.com/adobe/helix-html-pipeline/issues/286) [#287](https://github.com/adobe/helix-html-pipeline/issues/287)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### BREAKING CHANGES
|
|
12
17
|
|
|
13
|
-
*
|
|
18
|
+
* former x-auth-* headers are no longer returned thus also needs new .hlx.live logic
|
|
14
19
|
|
|
15
20
|
## [3.11.20](https://github.com/adobe/helix-html-pipeline/compare/v3.11.19...v3.11.20) (2023-07-11)
|
|
16
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/helix-html-pipeline",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.1",
|
|
4
4
|
"description": "Helix HTML Pipeline",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@semantic-release/git": "10.0.1",
|
|
74
74
|
"@semantic-release/npm": "10.0.4",
|
|
75
75
|
"c8": "8.0.0",
|
|
76
|
-
"eslint": "8.
|
|
76
|
+
"eslint": "8.45.0",
|
|
77
77
|
"eslint-import-resolver-exports": "1.0.0-beta.5",
|
|
78
78
|
"eslint-plugin-header": "3.1.1",
|
|
79
79
|
"eslint-plugin-import": "2.27.5",
|
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, FormsMessageDispatcher, PipelineTimer} from "./index";
|
|
12
|
+
import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer, AuthEnvLoader } from "./index";
|
|
13
13
|
import {PipelineContent} from "./PipelineContent";
|
|
14
14
|
import {Modifiers} from './utils/modifiers';
|
|
15
15
|
|
|
@@ -24,6 +24,8 @@ type Fetch = (url: string|Request, options?: RequestOptions) => Promise<Response
|
|
|
24
24
|
declare interface AccessConfig {
|
|
25
25
|
allow:(string|string[]);
|
|
26
26
|
|
|
27
|
+
apiKeyId:(string|string[]);
|
|
28
|
+
|
|
27
29
|
require: {
|
|
28
30
|
repository:(string|string[]);
|
|
29
31
|
};
|
|
@@ -40,6 +42,7 @@ declare interface PipelineOptions {
|
|
|
40
42
|
log: Console;
|
|
41
43
|
s3Loader: S3Loader;
|
|
42
44
|
messageDispatcher: FormsMessageDispatcher;
|
|
45
|
+
authEnvLoader: AuthEnvLoader;
|
|
43
46
|
fetch: Fetch;
|
|
44
47
|
owner: string;
|
|
45
48
|
repo: string;
|
|
@@ -59,6 +62,7 @@ declare class PipelineState {
|
|
|
59
62
|
contentBusId: string;
|
|
60
63
|
s3Loader: S3Loader;
|
|
61
64
|
messageDispatcher: FormsMessageDispatcher;
|
|
65
|
+
authEnvLoader: AuthEnvLoader;
|
|
62
66
|
fetch: Fetch;
|
|
63
67
|
|
|
64
68
|
/**
|
package/src/PipelineState.js
CHANGED
package/src/html-pipe.js
CHANGED
|
@@ -82,7 +82,7 @@ export async function htmlPipe(state, req) {
|
|
|
82
82
|
|
|
83
83
|
// check if .auth request
|
|
84
84
|
if (state.partition === '.auth' || state.info.path === '/.auth') {
|
|
85
|
-
if (!initAuthRoute(state, req, res)) {
|
|
85
|
+
if (!await initAuthRoute(state, req, res)) {
|
|
86
86
|
return res;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -118,7 +118,7 @@ export async function htmlPipe(state, req) {
|
|
|
118
118
|
]);
|
|
119
119
|
|
|
120
120
|
await requireProject(state, req, res);
|
|
121
|
-
if (
|
|
121
|
+
if (res.error !== 401) {
|
|
122
122
|
await authenticate(state, req, res);
|
|
123
123
|
}
|
|
124
124
|
|
package/src/index.d.ts
CHANGED
|
@@ -88,6 +88,16 @@ declare interface S3Loader {
|
|
|
88
88
|
headObject(bucketId, key): Promise<PipelineResponse>;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
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
|
+
|
|
91
101
|
declare interface DispatchMessageResponse {
|
|
92
102
|
messageId:string,
|
|
93
103
|
requestId:string,
|
|
@@ -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 { getAuthInfo } from '../utils/auth.js';
|
|
12
|
+
import { getAuthInfo, makeAuthError } from '../utils/auth.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Checks if the given email is allowed.
|
|
@@ -36,11 +36,9 @@ export function isAllowed(email = '', allows = []) {
|
|
|
36
36
|
* @returns {Promise<void>}
|
|
37
37
|
*/
|
|
38
38
|
export async function authenticate(state, req, res) {
|
|
39
|
-
// get auth info
|
|
40
|
-
const authInfo = await getAuthInfo(state, req);
|
|
41
|
-
|
|
42
39
|
// check if `.auth` route to validate and exchange token
|
|
43
40
|
if (state.info.path === '/.auth') {
|
|
41
|
+
const authInfo = await getAuthInfo(state, req);
|
|
44
42
|
await authInfo.exchangeToken(state, req, res);
|
|
45
43
|
return;
|
|
46
44
|
}
|
|
@@ -50,41 +48,55 @@ export async function authenticate(state, req, res) {
|
|
|
50
48
|
return;
|
|
51
49
|
}
|
|
52
50
|
|
|
51
|
+
// get auth info
|
|
52
|
+
const authInfo = await getAuthInfo(state, req);
|
|
53
|
+
|
|
53
54
|
// if not authenticated, redirect to login screen
|
|
54
55
|
if (!authInfo.authenticated) {
|
|
55
56
|
// send 401 for plain requests
|
|
56
57
|
if (state.info.selector || state.type !== 'html') {
|
|
57
58
|
state.log.warn('[auth] unauthorized. redirect to login only for extension less html.');
|
|
58
|
-
res
|
|
59
|
-
res.error = 'unauthorized.';
|
|
59
|
+
makeAuthError(state, req, res, 'unauthorized');
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
-
authInfo.redirectToLogin(state, req, res);
|
|
62
|
+
await authInfo.redirectToLogin(state, req, res);
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const { sub, jti, email } = authInfo.profile;
|
|
67
|
+
|
|
68
|
+
// validate subject, if present
|
|
69
|
+
if (sub) {
|
|
70
|
+
const [owner, repo] = sub.split('/');
|
|
71
|
+
if (owner !== state.owner || (repo !== '*' && repo !== state.repo)) {
|
|
72
|
+
state.log.warn(`[auth] invalid subject ${sub}: does not match ${state.owner}/${state.repo}`);
|
|
73
|
+
makeAuthError(state, req, res, 'invalid-subject');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// validate jti
|
|
79
|
+
if (jti) {
|
|
80
|
+
const ids = Array.isArray(state.config.access.apiKeyId)
|
|
81
|
+
? state.config.access.apiKeyId
|
|
82
|
+
: [state.config.access.apiKeyId];
|
|
83
|
+
if (ids.indexOf(jti) < 0) {
|
|
84
|
+
state.log.warn(`[auth] invalid jti ${jti}: does not match configured id ${state.config.access.apiKeyId}`);
|
|
85
|
+
makeAuthError(state, req, res, 'invalid-jti');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
// check profile is allowed
|
|
67
90
|
const { allow } = state.config.access;
|
|
68
91
|
const allows = Array.isArray(allow) ? allow : [allow];
|
|
69
|
-
if (!isAllowed(
|
|
92
|
+
if (!isAllowed(email, allows)) {
|
|
70
93
|
state.log.warn(`[auth] profile not allowed for ${allows}`);
|
|
71
|
-
res
|
|
72
|
-
res.error = 'forbidden.';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// set some response headers for deferred edge authentication
|
|
76
|
-
// AdobePatentID="P11443-US"
|
|
77
|
-
res.headers.set('x-hlx-auth-allow', allows.join(','));
|
|
78
|
-
if (authInfo.profile) {
|
|
79
|
-
res.headers.set('x-hlx-auth-iss', authInfo.profile.iss);
|
|
80
|
-
res.headers.set('x-hlx-auth-kid', authInfo.profile.kid);
|
|
81
|
-
res.headers.set('x-hlx-auth-aud', authInfo.profile.aud);
|
|
82
|
-
res.headers.set('x-hlx-auth-key', authInfo.profile.pem);
|
|
94
|
+
makeAuthError(state, req, res, 'forbidden', 403);
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
/**
|
|
87
|
-
* Checks if the given owner repo is
|
|
99
|
+
* Checks if the given owner repo is allowed
|
|
88
100
|
* @param {string} owner
|
|
89
101
|
* @param {string} repo
|
|
90
102
|
* @param {string[]} allows
|
package/src/utils/auth.d.ts
CHANGED
|
@@ -37,9 +37,14 @@ export declare interface IDPConfig {
|
|
|
37
37
|
|
|
38
38
|
export declare interface UserProfile {
|
|
39
39
|
email:string;
|
|
40
|
-
|
|
41
|
-
// picture:string;
|
|
40
|
+
|
|
42
41
|
iss:string;
|
|
42
|
+
|
|
43
|
+
aud:string;
|
|
44
|
+
|
|
45
|
+
sub: string;
|
|
46
|
+
|
|
47
|
+
jti: string;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export declare class AuthInfo {
|
|
@@ -68,7 +73,7 @@ export declare class AuthInfo {
|
|
|
68
73
|
* @param {PipelineRequest} req
|
|
69
74
|
* @param {PipelineResponse} res
|
|
70
75
|
*/
|
|
71
|
-
redirectToLogin(state, req, res);
|
|
76
|
+
async redirectToLogin(state, req, res);
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
79
|
* Performs a token exchange from the code flow and redirects to the root page
|
package/src/utils/auth.js
CHANGED
|
@@ -11,65 +11,88 @@
|
|
|
11
11
|
*/
|
|
12
12
|
// eslint-disable-next-line max-classes-per-file
|
|
13
13
|
import {
|
|
14
|
-
createLocalJWKSet,
|
|
14
|
+
createLocalJWKSet,
|
|
15
|
+
decodeJwt,
|
|
16
|
+
jwtVerify,
|
|
17
|
+
SignJWT,
|
|
18
|
+
importJWK,
|
|
15
19
|
} from 'jose';
|
|
16
20
|
import { clearAuthCookie, getAuthCookie, setAuthCookie } from './auth-cookie.js';
|
|
17
21
|
|
|
18
22
|
import idpMicrosoft from './idp-configs/microsoft.js';
|
|
19
|
-
import idpAdmin from './idp-configs/admin.js';
|
|
20
23
|
|
|
21
24
|
// eslint-disable-next-line import/no-unresolved
|
|
22
25
|
import cryptoImpl from '#crypto';
|
|
23
26
|
|
|
24
|
-
export const IDPS = [
|
|
25
|
-
idpMicrosoft,
|
|
26
|
-
idpAdmin,
|
|
27
|
-
];
|
|
28
|
-
|
|
29
27
|
const AUTH_REDIRECT_URL = 'https://login.hlx.page/.auth';
|
|
30
28
|
|
|
29
|
+
let ADMIN_KEY_PAIR = null;
|
|
30
|
+
|
|
31
31
|
export class AccessDeniedError extends Error {
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Signs the given JWT with the admin private key and returns the token.
|
|
36
|
+
* @param {PipelineState} state
|
|
37
|
+
* @param {SignJWT} jwt
|
|
38
|
+
* @returns {Promise<string>}
|
|
39
|
+
*/
|
|
40
|
+
async function signJWT(state, jwt) {
|
|
41
|
+
if (!ADMIN_KEY_PAIR) {
|
|
42
|
+
ADMIN_KEY_PAIR = {
|
|
43
|
+
privateKey: await importJWK(JSON.parse(state.env.HLX_ADMIN_IDP_PRIVATE_KEY), 'RS256'),
|
|
44
|
+
publicKey: JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const { privateKey, publicKey } = ADMIN_KEY_PAIR;
|
|
48
|
+
return jwt
|
|
49
|
+
.setProtectedHeader({
|
|
50
|
+
alg: 'RS256',
|
|
51
|
+
kid: publicKey.kid,
|
|
52
|
+
})
|
|
53
|
+
.setAudience(state.env.HLX_SITE_APP_AZURE_CLIENT_ID)
|
|
54
|
+
.setIssuer(publicKey.issuer)
|
|
55
|
+
.sign(privateKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Verifies and decodes the given jwt using the admin public key
|
|
60
|
+
* @param {PipelineState} state
|
|
61
|
+
* @param {string} jwt
|
|
62
|
+
* @param {boolean} lenient
|
|
63
|
+
* @returns {Promise<JWTPayload>}
|
|
64
|
+
*/
|
|
65
|
+
async function verifyJwt(state, jwt, lenient = false) {
|
|
66
|
+
const publicKey = JSON.parse(state.env.HLX_ADMIN_IDP_PUBLIC_KEY);
|
|
67
|
+
const jwks = createLocalJWKSet({
|
|
68
|
+
keys: [publicKey],
|
|
69
|
+
});
|
|
70
|
+
const { payload } = await jwtVerify(jwt, jwks, {
|
|
71
|
+
audience: state.env.HLX_SITE_APP_AZURE_CLIENT_ID,
|
|
72
|
+
issuer: publicKey.issuer,
|
|
73
|
+
clockTolerance: lenient ? 7 * 24 * 60 * 60 : 0,
|
|
74
|
+
});
|
|
75
|
+
return payload;
|
|
76
|
+
}
|
|
77
|
+
|
|
34
78
|
/**
|
|
35
79
|
* Decodes the given id_token for the given idp. if `lenient` is `true`, the clock tolerance
|
|
36
80
|
* is set to 1 week. this allows to extract some profile information that can be used as login_hint.
|
|
37
81
|
* @param {PipelineState} state
|
|
38
|
-
* @param {IDPConfig} idp
|
|
39
82
|
* @param {string} idToken
|
|
40
83
|
* @param {boolean} lenient
|
|
41
84
|
* @returns {Promise<JWTPayload>}
|
|
42
85
|
*/
|
|
43
|
-
export async function decodeIdToken(state,
|
|
86
|
+
export async function decodeIdToken(state, idToken, lenient = false) {
|
|
44
87
|
const { log } = state;
|
|
45
|
-
const
|
|
46
|
-
? createLocalJWKSet(idp.discovery.jwks)
|
|
47
|
-
: /* c8 ignore next */ createRemoteJWKSet(new URL(idp.discovery.jwks_uri));
|
|
48
|
-
|
|
49
|
-
const { payload, key, protectedHeader } = await jwtVerify(idToken, jwks, {
|
|
50
|
-
audience: idp.client(state).clientId,
|
|
51
|
-
clockTolerance: lenient ? 7 * 24 * 60 * 60 : 0,
|
|
52
|
-
});
|
|
88
|
+
const payload = await verifyJwt(state, idToken, lenient);
|
|
53
89
|
|
|
54
90
|
// delete from information not needed in the profile
|
|
55
|
-
['azp', '
|
|
91
|
+
['azp', 'at_hash', 'nonce', 'aio', 'c_hash'].forEach((prop) => delete payload[prop]);
|
|
56
92
|
|
|
57
93
|
// compute ttl
|
|
58
94
|
payload.ttl = payload.exp - Math.floor(Date.now() / 1000);
|
|
59
95
|
|
|
60
|
-
// export the public key
|
|
61
|
-
payload.pem = await exportSPKI(key);
|
|
62
|
-
// and encode it base64 url
|
|
63
|
-
/* c8 ignore next 3 */
|
|
64
|
-
if (typeof Buffer === 'undefined') {
|
|
65
|
-
// non-node runtime
|
|
66
|
-
payload.pem = btoa(payload.pem);
|
|
67
|
-
} else {
|
|
68
|
-
// node runtime
|
|
69
|
-
payload.pem = Buffer.from(payload.pem, 'utf-8').toString('base64url');
|
|
70
|
-
}
|
|
71
|
-
payload.kid = protectedHeader.kid;
|
|
72
|
-
|
|
73
96
|
log.info(`[auth] decoded id_token${lenient ? ' (lenient)' : ''} from ${payload.iss} and validated payload.`);
|
|
74
97
|
return payload;
|
|
75
98
|
}
|
|
@@ -102,6 +125,22 @@ function getRequestHostAndProto(state, req) {
|
|
|
102
125
|
};
|
|
103
126
|
}
|
|
104
127
|
|
|
128
|
+
/**
|
|
129
|
+
* sets the auth error on the response and clears the cookie.
|
|
130
|
+
* @param state
|
|
131
|
+
* @param req
|
|
132
|
+
* @param res
|
|
133
|
+
* @param error
|
|
134
|
+
* @param status
|
|
135
|
+
*/
|
|
136
|
+
export function makeAuthError(state, req, res, error, status = 401) {
|
|
137
|
+
const { proto } = getRequestHostAndProto(state, req);
|
|
138
|
+
res.status = status;
|
|
139
|
+
res.error = error;
|
|
140
|
+
res.headers.set('set-cookie', clearAuthCookie(proto === 'https'));
|
|
141
|
+
res.headers.set('x-error', error);
|
|
142
|
+
}
|
|
143
|
+
|
|
105
144
|
/**
|
|
106
145
|
* AuthInfo class
|
|
107
146
|
*/
|
|
@@ -174,10 +213,11 @@ export class AuthInfo {
|
|
|
174
213
|
* @param {PipelineResponse} res
|
|
175
214
|
* @param {IDPConfig} idp IDP config
|
|
176
215
|
*/
|
|
177
|
-
redirectToLogin(state, req, res) {
|
|
216
|
+
async redirectToLogin(state, req, res) {
|
|
178
217
|
const { log } = state;
|
|
179
218
|
const { idp } = this;
|
|
180
219
|
|
|
220
|
+
await state.authEnvLoader.load(state);
|
|
181
221
|
const { clientId, clientSecret } = idp.client(state);
|
|
182
222
|
if (!clientId || !clientSecret) {
|
|
183
223
|
log.error('[auth] unable to create login redirect: missing client_id or client_secret');
|
|
@@ -191,23 +231,20 @@ export class AuthInfo {
|
|
|
191
231
|
const { host, proto } = getRequestHostAndProto(state, req);
|
|
192
232
|
if (!host) {
|
|
193
233
|
log.error('[auth] unable to create login redirect: no xfh or config.host.');
|
|
194
|
-
res
|
|
195
|
-
res.error = 'no host information.';
|
|
234
|
+
makeAuthError(state, req, res, 'no host information.');
|
|
196
235
|
return;
|
|
197
236
|
}
|
|
198
237
|
|
|
199
238
|
const url = new URL(idp.discovery.authorization_endpoint);
|
|
200
239
|
|
|
201
|
-
|
|
202
|
-
const tokenState = new UnsecuredJWT({
|
|
240
|
+
const tokenState = await signJWT(state, new SignJWT({
|
|
203
241
|
owner: state.owner,
|
|
204
242
|
repo: state.repo,
|
|
205
|
-
contentBusId: state.contentBusId,
|
|
206
243
|
// this is our own login redirect, i.e. the current document
|
|
207
244
|
requestPath: state.info.path,
|
|
208
245
|
requestHost: host,
|
|
209
246
|
requestProto: proto,
|
|
210
|
-
})
|
|
247
|
+
}));
|
|
211
248
|
|
|
212
249
|
url.searchParams.append('client_id', clientId);
|
|
213
250
|
url.searchParams.append('response_type', 'code');
|
|
@@ -240,11 +277,13 @@ export class AuthInfo {
|
|
|
240
277
|
const { code } = req.params;
|
|
241
278
|
if (!code) {
|
|
242
279
|
log.warn('[auth] code exchange failed: code parameter missing.');
|
|
243
|
-
res
|
|
244
|
-
res.error = 'code exchange failed.';
|
|
280
|
+
makeAuthError(state, req, res, 'code exchange failed.');
|
|
245
281
|
return;
|
|
246
282
|
}
|
|
247
283
|
|
|
284
|
+
// TODO: exchange token on the login host, set-cookie,
|
|
285
|
+
// and then again set-cookie on the request host
|
|
286
|
+
|
|
248
287
|
// ensure that the request is made to the target host
|
|
249
288
|
if (req.params.state?.requestHost) {
|
|
250
289
|
const { host } = getRequestHostAndProto(state, req);
|
|
@@ -263,6 +302,7 @@ export class AuthInfo {
|
|
|
263
302
|
}
|
|
264
303
|
}
|
|
265
304
|
|
|
305
|
+
await state.authEnvLoader.load(state);
|
|
266
306
|
const { clientId, clientSecret } = idp.client(state);
|
|
267
307
|
const url = new URL(idp.discovery.token_endpoint);
|
|
268
308
|
const body = {
|
|
@@ -282,22 +322,37 @@ export class AuthInfo {
|
|
|
282
322
|
});
|
|
283
323
|
if (!ret.ok) {
|
|
284
324
|
log.warn(`[auth] code exchange failed: ${ret.status}`, await ret.text());
|
|
285
|
-
res
|
|
286
|
-
res.error = 'code exchange failed.';
|
|
325
|
+
makeAuthError(state, req, res, 'code exchange failed.');
|
|
287
326
|
return;
|
|
288
327
|
}
|
|
289
328
|
|
|
290
329
|
const tokenResponse = await ret.json();
|
|
291
330
|
const { id_token: idToken } = tokenResponse;
|
|
331
|
+
let payload;
|
|
292
332
|
try {
|
|
293
|
-
|
|
333
|
+
payload = decodeJwt(idToken);
|
|
294
334
|
} catch (e) {
|
|
295
335
|
log.warn(`[auth] id token from ${idp.name} is invalid: ${e.message}`);
|
|
296
|
-
res
|
|
297
|
-
res.error = 'id token invalid.';
|
|
336
|
+
makeAuthError(state, req, res, 'id token invalid.');
|
|
298
337
|
return;
|
|
299
338
|
}
|
|
300
339
|
|
|
340
|
+
const email = payload.email || payload.preferred_username;
|
|
341
|
+
if (!email) {
|
|
342
|
+
log.warn(`[auth] id token from ${idp.name} is missing email or preferred_username`);
|
|
343
|
+
makeAuthError(state, req, res, 'id token invalid.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// create new token
|
|
348
|
+
const jwt = new SignJWT({
|
|
349
|
+
email,
|
|
350
|
+
name: payload.name,
|
|
351
|
+
})
|
|
352
|
+
.setIssuedAt()
|
|
353
|
+
.setExpirationTime('12 hours');
|
|
354
|
+
const authToken = await signJWT(state, jwt);
|
|
355
|
+
|
|
301
356
|
// ensure that auth cookie is not cleared again in `index.js`
|
|
302
357
|
// ctx.attributes.authInfo?.withCookieInvalid(false);
|
|
303
358
|
|
|
@@ -307,13 +362,13 @@ export class AuthInfo {
|
|
|
307
362
|
res.body = `please go to <a href="${location}">${location}</a>`;
|
|
308
363
|
res.headers.set('location', location);
|
|
309
364
|
res.headers.set('content-tye', 'text/plain');
|
|
310
|
-
res.headers.set('set-cookie', setAuthCookie(
|
|
365
|
+
res.headers.set('set-cookie', setAuthCookie(authToken, req.params.state.requestProto === 'https'));
|
|
311
366
|
res.headers.set('cache-control', 'no-store, private, must-revalidate');
|
|
312
367
|
res.error = 'moved';
|
|
313
368
|
}
|
|
314
369
|
}
|
|
315
370
|
|
|
316
|
-
export function initAuthRoute(state, req, res) {
|
|
371
|
+
export async function initAuthRoute(state, req, res) {
|
|
317
372
|
const { log } = state;
|
|
318
373
|
|
|
319
374
|
// use request headers if present
|
|
@@ -328,18 +383,18 @@ export function initAuthRoute(state, req, res) {
|
|
|
328
383
|
|
|
329
384
|
if (!req.params.state) {
|
|
330
385
|
log.warn('[auth] unable to exchange token: no state.');
|
|
331
|
-
res
|
|
332
|
-
res.headers.set('x-error', 'missing state parameter.');
|
|
386
|
+
makeAuthError(state, req, res, 'missing state parameter.');
|
|
333
387
|
return false;
|
|
334
388
|
}
|
|
335
389
|
|
|
336
390
|
try {
|
|
337
391
|
req.params.rawState = req.params.state;
|
|
338
|
-
req.params.state =
|
|
392
|
+
req.params.state = await verifyJwt(state, req.params.state);
|
|
393
|
+
delete req.params.state.aud;
|
|
394
|
+
delete req.params.state.iss;
|
|
339
395
|
} catch (e) {
|
|
340
396
|
log.warn(`[auth] error decoding state parameter: invalid state: ${e.message}`);
|
|
341
|
-
res
|
|
342
|
-
res.headers.set('x-error', 'missing state parameter.');
|
|
397
|
+
makeAuthError(state, req, res, 'missing state parameter.');
|
|
343
398
|
return false;
|
|
344
399
|
}
|
|
345
400
|
|
|
@@ -347,7 +402,6 @@ export function initAuthRoute(state, req, res) {
|
|
|
347
402
|
state.owner = req.params.state.owner;
|
|
348
403
|
state.repo = req.params.state.repo;
|
|
349
404
|
state.ref = 'main';
|
|
350
|
-
state.contentBusId = req.params.state.contentBusId;
|
|
351
405
|
state.partition = 'preview';
|
|
352
406
|
state.info.path = '/.auth';
|
|
353
407
|
return true;
|
|
@@ -374,31 +428,18 @@ async function getAuthInfoFromCookieOrHeader(state, req) {
|
|
|
374
428
|
}
|
|
375
429
|
}
|
|
376
430
|
if (idToken) {
|
|
377
|
-
let idp;
|
|
378
431
|
try {
|
|
379
|
-
const { iss } = decodeJwt(idToken);
|
|
380
|
-
if (!iss) {
|
|
381
|
-
log.warn('[auth] missing \'iss\' claim in id_token.');
|
|
382
|
-
return AuthInfo.Default().withCookieInvalid(true);
|
|
383
|
-
}
|
|
384
|
-
idp = IDPS.find((i) => i.validateIssuer(iss));
|
|
385
|
-
if (!idp) {
|
|
386
|
-
log.warn(`[auth] no IDP found for: ${iss}`);
|
|
387
|
-
return AuthInfo.Default().withCookieInvalid(true);
|
|
388
|
-
}
|
|
389
432
|
return AuthInfo.Default()
|
|
390
|
-
.withProfile(await decodeIdToken(state,
|
|
433
|
+
.withProfile(await decodeIdToken(state, idToken))
|
|
391
434
|
.withAuthenticated(true)
|
|
392
|
-
.withIdp(idp)
|
|
393
435
|
.withIdToken(idToken);
|
|
394
436
|
} catch (e) {
|
|
395
|
-
if (e.code === 'ERR_JWT_EXPIRED'
|
|
437
|
+
if (e.code === 'ERR_JWT_EXPIRED') {
|
|
396
438
|
try {
|
|
397
|
-
const profile = await decodeIdToken(state,
|
|
439
|
+
const profile = await decodeIdToken(state, idToken, true);
|
|
398
440
|
log.warn(`[auth] decoding the id_token failed: ${e.message}, using expired token as hint.`);
|
|
399
441
|
return AuthInfo.Default()
|
|
400
442
|
.withExpired(true)
|
|
401
|
-
.withIdp(idp)
|
|
402
443
|
.withLoginHint(profile.email);
|
|
403
444
|
} catch {
|
|
404
445
|
// ignore
|
|
@@ -17,7 +17,7 @@ export default {
|
|
|
17
17
|
clientSecret: state.env.HLX_SITE_APP_AZURE_CLIENT_SECRET,
|
|
18
18
|
}),
|
|
19
19
|
scope: 'openid profile email',
|
|
20
|
-
validateIssuer: (iss) => iss.startsWith('https://login.microsoftonline.com/'),
|
|
20
|
+
// validateIssuer: (iss) => iss.startsWith('https://login.microsoftonline.com/'),
|
|
21
21
|
discoveryUrl: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
|
|
22
22
|
// todo: fetch from discovery document
|
|
23
23
|
discovery: {
|
|
@@ -1,25 +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
|
-
* virtual idp config for the admin service
|
|
15
|
-
*/
|
|
16
|
-
export default {
|
|
17
|
-
name: 'admin',
|
|
18
|
-
client: (state) => ({
|
|
19
|
-
clientId: state.env.HLX_SITE_APP_AZURE_CLIENT_ID,
|
|
20
|
-
}),
|
|
21
|
-
validateIssuer: (iss) => iss === 'https://admin.hlx.page/',
|
|
22
|
-
discovery: {
|
|
23
|
-
jwks_uri: 'https://admin.hlx.page/auth/discovery/keys',
|
|
24
|
-
},
|
|
25
|
-
};
|