@infoxchange/make-it-so 2.10.0-internal-testing-vdt-199-add-oidc-auth.1 → 2.10.0-internal-testing-vdt-199-add-oidc-auth.3
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/README.md +2 -0
- package/commitlint.config.ts +14 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-check.js +18 -18
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-route.d.ts.map +1 -1
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-route.js +6 -6
- package/dist/cdk-constructs/CloudWatchOidcAuth/index.d.ts +1 -1
- package/dist/cdk-constructs/CloudWatchOidcAuth/index.d.ts.map +1 -1
- package/dist/cdk-constructs/CloudWatchOidcAuth/index.js +36 -31
- package/dist/lib/utils/source-code.d.ts +1 -1
- package/dist/lib/utils/source-code.d.ts.map +1 -1
- package/dist/lib/utils/source-code.js +1 -9
- package/package.json +1 -1
- package/src/cdk-constructs/CloudWatchOidcAuth/auth-check.ts +106 -112
- package/src/cdk-constructs/CloudWatchOidcAuth/auth-route.ts +20 -21
- package/src/cdk-constructs/CloudWatchOidcAuth/index.ts +159 -101
- package/src/lib/utils/source-code.ts +0 -11
package/README.md
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UserConfig } from "@commitlint/types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
ignores: [
|
|
5
|
+
(message) =>
|
|
6
|
+
// Allow "wip" commits except when publishing a production release or on PR CI jobs
|
|
7
|
+
process.env.GITHUB_EVENT_NAME !== "pull_request" &&
|
|
8
|
+
(process.env.GITHUB_WORKFLOW !== "Publish" ||
|
|
9
|
+
(process.env.GITHUB_REF_NAME?.startsWith("internal-testing-") ??
|
|
10
|
+
true)) &&
|
|
11
|
+
(message === "wip" || message.startsWith("wip:")),
|
|
12
|
+
],
|
|
13
|
+
extends: ["@commitlint/config-conventional"],
|
|
14
|
+
} satisfies UserConfig;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
// Based off: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_kvs_jwt_verify_section.html
|
|
4
|
-
import crypto from
|
|
4
|
+
import crypto from "crypto";
|
|
5
5
|
// @ts-expect-error -- This library only exists in the CloudFront Functions runtime that this code runs in
|
|
6
|
-
import cf from
|
|
6
|
+
import cf from "cloudfront";
|
|
7
7
|
//Response when JWT is not valid.
|
|
8
8
|
// const response401 = {
|
|
9
9
|
// statusCode: 401,
|
|
@@ -12,23 +12,23 @@ import cf from 'cloudfront';
|
|
|
12
12
|
const response401 = {
|
|
13
13
|
statusCode: 302,
|
|
14
14
|
headers: {
|
|
15
|
-
location: {
|
|
15
|
+
location: { value: "/auth/oidc/authorize" },
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
// Remember to associate the KVS with your function before calling the const kvsKey = 'jwt.secret'.
|
|
19
19
|
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html
|
|
20
|
-
const kvsKey =
|
|
20
|
+
const kvsKey = "__placeholder-for-jwt-secret-key__";
|
|
21
21
|
// set to true to enable console logging
|
|
22
22
|
const loggingEnabled = true; // false;
|
|
23
23
|
function jwt_decode(token, key, noVerify, algorithm) {
|
|
24
24
|
// check token
|
|
25
25
|
if (!token) {
|
|
26
|
-
throw new Error(
|
|
26
|
+
throw new Error("No token supplied");
|
|
27
27
|
}
|
|
28
28
|
// check segments
|
|
29
|
-
const segments = token.split(
|
|
29
|
+
const segments = token.split(".");
|
|
30
30
|
if (segments.length !== 3) {
|
|
31
|
-
throw new Error(
|
|
31
|
+
throw new Error("Not enough or too many segments");
|
|
32
32
|
}
|
|
33
33
|
// All segment should be base64
|
|
34
34
|
const headerSeg = segments[0];
|
|
@@ -37,20 +37,20 @@ function jwt_decode(token, key, noVerify, algorithm) {
|
|
|
37
37
|
// base64 decode and parse JSON
|
|
38
38
|
const payload = JSON.parse(_base64urlDecode(payloadSeg));
|
|
39
39
|
if (!noVerify) {
|
|
40
|
-
const signingMethod =
|
|
41
|
-
const signingType =
|
|
40
|
+
const signingMethod = "sha256";
|
|
41
|
+
const signingType = "hmac";
|
|
42
42
|
// Verify signature. `sign` will return base64 string.
|
|
43
|
-
const signingInput = [headerSeg, payloadSeg].join(
|
|
43
|
+
const signingInput = [headerSeg, payloadSeg].join(".");
|
|
44
44
|
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
|
|
45
|
-
throw new Error(
|
|
45
|
+
throw new Error("Signature verification failed");
|
|
46
46
|
}
|
|
47
47
|
// Support for nbf and exp claims.
|
|
48
48
|
// According to the RFC, they should be in seconds.
|
|
49
49
|
if (payload.nbf && Date.now() < payload.nbf * 1000) {
|
|
50
|
-
throw new Error(
|
|
50
|
+
throw new Error("Token not yet active");
|
|
51
51
|
}
|
|
52
52
|
if (payload.exp && Date.now() > payload.exp * 1000) {
|
|
53
|
-
throw new Error(
|
|
53
|
+
throw new Error("Token expired");
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
return payload;
|
|
@@ -63,7 +63,7 @@ function _constantTimeEquals(a, b) {
|
|
|
63
63
|
}
|
|
64
64
|
let xor = 0;
|
|
65
65
|
for (let i = 0; i < a.length; i++) {
|
|
66
|
-
xor |=
|
|
66
|
+
xor |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
67
67
|
}
|
|
68
68
|
return 0 === xor;
|
|
69
69
|
}
|
|
@@ -72,17 +72,17 @@ function _verify(input, key, method, type, signature) {
|
|
|
72
72
|
return _constantTimeEquals(signature, _sign(input, key, method));
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
|
-
throw new Error(
|
|
75
|
+
throw new Error("Algorithm type not recognized");
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
function _sign(input, key, method) {
|
|
79
|
-
return crypto.createHmac(method, key).update(input).digest(
|
|
79
|
+
return crypto.createHmac(method, key).update(input).digest("base64url");
|
|
80
80
|
}
|
|
81
81
|
function _base64urlDecode(str) {
|
|
82
|
-
return Buffer.from(str,
|
|
82
|
+
return Buffer.from(str, "base64url");
|
|
83
83
|
}
|
|
84
84
|
async function handler(event) {
|
|
85
|
-
|
|
85
|
+
const request = event.request;
|
|
86
86
|
//Secret key used to verify JWT token.
|
|
87
87
|
//Update with your own key.
|
|
88
88
|
const secret_key = await getSecret();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-route.d.ts","sourceRoot":"","sources":["../../../src/cdk-constructs/CloudWatchOidcAuth/auth-route.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-route.d.ts","sourceRoot":"","sources":["../../../src/cdk-constructs/CloudWatchOidcAuth/auth-route.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,OAAO,4CAiDnB,CAAC"}
|
|
@@ -33,14 +33,14 @@ export const handler = convertApiGatewayHandlerToCloudFrontHandler(AuthHandler({
|
|
|
33
33
|
};
|
|
34
34
|
// Options (optional)
|
|
35
35
|
const options = {
|
|
36
|
-
algorithm:
|
|
37
|
-
expiresIn:
|
|
36
|
+
algorithm: "HS256",
|
|
37
|
+
expiresIn: "1h",
|
|
38
38
|
};
|
|
39
39
|
// Create the token
|
|
40
40
|
const token = jwt.sign(payload, jwtSecret, options);
|
|
41
41
|
const expires = new Date(
|
|
42
42
|
// @ ts-ignore error in GH action
|
|
43
|
-
Date.now() +
|
|
43
|
+
Date.now() + 1000 * 60 * 60 * 24 * 7);
|
|
44
44
|
return {
|
|
45
45
|
statusCode: 302,
|
|
46
46
|
headers: {
|
|
@@ -58,7 +58,7 @@ export const handler = convertApiGatewayHandlerToCloudFrontHandler(AuthHandler({
|
|
|
58
58
|
// },
|
|
59
59
|
// });
|
|
60
60
|
},
|
|
61
|
-
})
|
|
61
|
+
}),
|
|
62
62
|
},
|
|
63
63
|
}));
|
|
64
64
|
// @ts-expect-error - testing
|
|
@@ -66,8 +66,8 @@ function convertApiGatewayHandlerToCloudFrontHandler(callback) {
|
|
|
66
66
|
// @ts-expect-error - testing
|
|
67
67
|
return async function (event, context) {
|
|
68
68
|
// Used by AuthHandler to create callback url sent to oidc server
|
|
69
|
-
event.requestContext.domainName = event.headers[
|
|
70
|
-
console.log(
|
|
69
|
+
event.requestContext.domainName = event.headers["x-forwarded-host"];
|
|
70
|
+
console.log("----", event, context);
|
|
71
71
|
// console.log("event", event)
|
|
72
72
|
// console.log("context", context)
|
|
73
73
|
const response = await callback(event, context);
|
|
@@ -16,7 +16,7 @@ export declare class CloudWatchOidcAuth extends Construct {
|
|
|
16
16
|
readonly oidcScope: string;
|
|
17
17
|
readonly id: string;
|
|
18
18
|
constructor(scope: ConstructScope, id: ConstructId, props: Props);
|
|
19
|
-
addToDistributionDefinition<DistributionProps extends BaseSiteCdkDistributionProps>(scope: ConstructScope, { distributionDefinition, prefix }: {
|
|
19
|
+
addToDistributionDefinition<DistributionProps extends BaseSiteCdkDistributionProps>(scope: ConstructScope, { distributionDefinition, prefix, }: {
|
|
20
20
|
distributionDefinition: Mutable<DistributionProps>;
|
|
21
21
|
prefix?: string;
|
|
22
22
|
}): Mutable<DistributionProps>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cdk-constructs/CloudWatchOidcAuth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cdk-constructs/CloudWatchOidcAuth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AASvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAI1E,KAAK,cAAc,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,KAAK,WAAW,GAAG,qBAAqB,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9D,KAAK,OAAO,CAAC,CAAC,IAAI;IAChB,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAC/B,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,SAAS;IAC/C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAER,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;IAQhE,2BAA2B,CACzB,iBAAiB,SAAS,4BAA4B,EAEtD,KAAK,EAAE,cAAc,EACrB,EACE,sBAAsB,EACtB,MAAgB,GACjB,EAAE;QAAE,sBAAsB,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IA6C5E,OAAO,CAAC,sBAAsB;IAyH9B,OAAO,CAAC,sBAAsB;CA0E/B"}
|
|
@@ -2,12 +2,13 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import SecretsManager from "aws-cdk-lib/aws-secretsmanager";
|
|
3
3
|
import CloudFront from "aws-cdk-lib/aws-cloudfront";
|
|
4
4
|
import CDK from "aws-cdk-lib";
|
|
5
|
-
import CdkCustomResources from
|
|
6
|
-
import Lambda from
|
|
7
|
-
import { getFileContentsWithoutTypes } from "../../lib/utils/source-code.js";
|
|
5
|
+
import CdkCustomResources from "aws-cdk-lib/custom-resources";
|
|
6
|
+
import Lambda from "aws-cdk-lib/aws-lambda";
|
|
8
7
|
import * as SST from "sst/constructs";
|
|
9
8
|
import { Config as SSTInternalConfig } from "sst/config.js";
|
|
10
9
|
import CloudFrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import fs from "node:fs";
|
|
11
12
|
export class CloudWatchOidcAuth extends Construct {
|
|
12
13
|
oidcIssuerUrl;
|
|
13
14
|
oidcClientId;
|
|
@@ -20,23 +21,24 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
20
21
|
this.oidcScope = props.oidcScope;
|
|
21
22
|
this.id = id;
|
|
22
23
|
}
|
|
23
|
-
addToDistributionDefinition(scope, { distributionDefinition, prefix = "/auth" }) {
|
|
24
|
+
addToDistributionDefinition(scope, { distributionDefinition, prefix = "/auth", }) {
|
|
24
25
|
console.log("------", import.meta.dirname, import.meta.url, import.meta.filename);
|
|
25
26
|
const updatedDistributionDefinition = { ...distributionDefinition };
|
|
26
|
-
const behaviourName = `${prefix.replace(/^\//g,
|
|
27
|
-
updatedDistributionDefinition.additionalBehaviors =
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const behaviourName = `${prefix.replace(/^\//g, "")}/*`;
|
|
28
|
+
updatedDistributionDefinition.additionalBehaviors =
|
|
29
|
+
updatedDistributionDefinition.additionalBehaviors
|
|
30
|
+
? { ...updatedDistributionDefinition.additionalBehaviors }
|
|
31
|
+
: {};
|
|
30
32
|
if (updatedDistributionDefinition.additionalBehaviors[behaviourName]) {
|
|
31
33
|
throw new Error(`Behavior for prefix ${prefix} already exists in distribution definition`);
|
|
32
34
|
}
|
|
33
35
|
const jwtSecret = new SecretsManager.Secret(this, `${this.id}JwtSecret`, {
|
|
34
|
-
description:
|
|
36
|
+
description: "JWT Signing Secret",
|
|
35
37
|
generateSecretString: {
|
|
36
38
|
passwordLength: 32,
|
|
37
39
|
excludePunctuation: true,
|
|
38
40
|
includeSpace: false,
|
|
39
|
-
requireEachIncludedType: true
|
|
41
|
+
requireEachIncludedType: true,
|
|
40
42
|
},
|
|
41
43
|
// Secret is only used for sessions so it's safe to delete on stack removal
|
|
42
44
|
removalPolicy: CDK.RemovalPolicy.DESTROY,
|
|
@@ -44,11 +46,13 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
44
46
|
updatedDistributionDefinition.defaultBehavior = {
|
|
45
47
|
...updatedDistributionDefinition.defaultBehavior,
|
|
46
48
|
functionAssociations: [
|
|
47
|
-
...(updatedDistributionDefinition.defaultBehavior
|
|
48
|
-
|
|
49
|
+
...(updatedDistributionDefinition.defaultBehavior
|
|
50
|
+
?.functionAssociations || []),
|
|
51
|
+
this.getFunctionAssociation(scope, jwtSecret),
|
|
49
52
|
],
|
|
50
53
|
};
|
|
51
|
-
updatedDistributionDefinition.additionalBehaviors[behaviourName] =
|
|
54
|
+
updatedDistributionDefinition.additionalBehaviors[behaviourName] =
|
|
55
|
+
this.getAuthBehaviorOptions(scope, jwtSecret, prefix);
|
|
52
56
|
return updatedDistributionDefinition;
|
|
53
57
|
}
|
|
54
58
|
getFunctionAssociation(scope, jwtSecret) {
|
|
@@ -60,8 +64,9 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
60
64
|
const getEtag = new CdkCustomResources.AwsCustomResource(this, `${this.id}GetKVStoreEtag`, {
|
|
61
65
|
installLatestAwsSdk: false, // No real benefit in our case for the cost of a longer execution time
|
|
62
66
|
onUpdate: {
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
68
|
+
service: "@aws-sdk/client-cloudfront-keyvaluestore",
|
|
69
|
+
action: "describeKeyValueStore",
|
|
65
70
|
parameters: { KvsARN: kvsArn },
|
|
66
71
|
// We include a timestamp in the physicalResourceId to ensure we fetch the latest etag on every update
|
|
67
72
|
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(`${kvStoreId}-etag-${Date.now()}`),
|
|
@@ -70,7 +75,7 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
70
75
|
resources: [kvsArn],
|
|
71
76
|
}),
|
|
72
77
|
});
|
|
73
|
-
const etag = getEtag.getResponseField(
|
|
78
|
+
const etag = getEtag.getResponseField("ETag");
|
|
74
79
|
// An annoying limitation of CloudFormation is that it won't resolve dynamic references for secrets when
|
|
75
80
|
// used as a parameter to a custom resource. To get around this we manually resolve it with another custom
|
|
76
81
|
// resource. Note this won't result in the secret being exposed in CloudFormation templates but it will
|
|
@@ -81,8 +86,8 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
81
86
|
installLatestAwsSdk: false,
|
|
82
87
|
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
83
88
|
onUpdate: {
|
|
84
|
-
service:
|
|
85
|
-
action:
|
|
89
|
+
service: "@aws-sdk/client-secrets-manager",
|
|
90
|
+
action: "getSecretValue",
|
|
86
91
|
parameters: {
|
|
87
92
|
SecretId: jwtSecret.secretArn,
|
|
88
93
|
},
|
|
@@ -97,12 +102,13 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
97
102
|
const putKeyValue = new CdkCustomResources.AwsCustomResource(this, `${this.id}PutKeyValue`, {
|
|
98
103
|
installLatestAwsSdk: false, // No real benefit in our case for the cost of a longer execution time
|
|
99
104
|
onUpdate: {
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
106
|
+
service: "@aws-sdk/client-cloudfront-keyvaluestore",
|
|
107
|
+
action: "putKey",
|
|
102
108
|
parameters: {
|
|
103
109
|
KvsARN: kvsArn,
|
|
104
110
|
Key: key,
|
|
105
|
-
Value: secretValue.getResponseField(
|
|
111
|
+
Value: secretValue.getResponseField("SecretString"),
|
|
106
112
|
IfMatch: etag,
|
|
107
113
|
},
|
|
108
114
|
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(`${kvStoreId}-${key}`),
|
|
@@ -115,16 +121,15 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
115
121
|
// as well. But AwsCustomResource doesn't give us direct access to the underlying Lambda function so we inject a
|
|
116
122
|
// NODE_OPTIONS env var to import on start. At some point AwsCustomResource will presumably switch to a later node
|
|
117
123
|
// version and we might need to update this to '--import=' instead of '--require='.
|
|
118
|
-
const fn = putKeyValue.node.findChild(
|
|
124
|
+
const fn = putKeyValue.node.findChild("Provider");
|
|
119
125
|
if (!(fn instanceof Lambda.SingletonFunction)) {
|
|
120
126
|
throw new Error("Could not find the underlying Lambda function of the AwsCustomResource");
|
|
121
127
|
}
|
|
122
|
-
fn.addEnvironment(
|
|
128
|
+
fn.addEnvironment("NODE_OPTIONS", "--require=@aws-sdk/signature-v4-crt");
|
|
123
129
|
const edgeFuncAuthCheck = new CloudFront.Function(scope, `${this.id}EdgeFunctionAuthCheck`, {
|
|
124
|
-
code: CloudFront.FunctionCode.fromInline(
|
|
125
|
-
.replace("__placeholder-for-jwt-secret-key__", key)),
|
|
130
|
+
code: CloudFront.FunctionCode.fromInline(fs.readFileSync(path.join(import.meta.dirname, "auth-check.js"), "utf8").replace("__placeholder-for-jwt-secret-key__", key)),
|
|
126
131
|
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
127
|
-
keyValueStore: cfKeyValueStore
|
|
132
|
+
keyValueStore: cfKeyValueStore,
|
|
128
133
|
});
|
|
129
134
|
return {
|
|
130
135
|
function: edgeFuncAuthCheck,
|
|
@@ -134,13 +139,13 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
134
139
|
getAuthBehaviorOptions(scope, jwtSecret, prefix) {
|
|
135
140
|
const edgeFuncAuth = new SST.Function(scope, `${this.id}EdgeFunctionAuth`, {
|
|
136
141
|
runtime: "nodejs20.x",
|
|
137
|
-
handler: "
|
|
142
|
+
handler: path.join(import.meta.dirname, "auth-route.handler"),
|
|
138
143
|
environment: {
|
|
139
144
|
OIDC_ISSUER_URL: this.oidcIssuerUrl,
|
|
140
145
|
OIDC_CLIENT_ID: this.oidcClientId,
|
|
141
146
|
OIDC_SCOPE: this.oidcScope,
|
|
142
147
|
JWT_SECRET: jwtSecret.secretValue.toString(),
|
|
143
|
-
}
|
|
148
|
+
},
|
|
144
149
|
});
|
|
145
150
|
// edgeFuncAuth uses SST's AuthHandler construct which is normally run inside a lambda that's
|
|
146
151
|
// created by SST's Auth construct. AuthHandler expects certain environment variables to be set
|
|
@@ -168,8 +173,8 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
168
173
|
origin: new CloudFrontOrigins.HttpOrigin(CDK.Fn.parseDomainName(edgeFuncAuthUrl.url)),
|
|
169
174
|
allowedMethods: CloudFront.AllowedMethods.ALLOW_ALL,
|
|
170
175
|
cachePolicy: new CloudFront.CachePolicy(scope, `${this.id}AllowAllCookiesPolicy`, {
|
|
171
|
-
cachePolicyName:
|
|
172
|
-
comment:
|
|
176
|
+
cachePolicyName: "AllowAllCookiesPolicy",
|
|
177
|
+
comment: "Cache policy that forwards all cookies",
|
|
173
178
|
defaultTtl: CDK.Duration.seconds(1),
|
|
174
179
|
minTtl: CDK.Duration.seconds(1),
|
|
175
180
|
maxTtl: CDK.Duration.seconds(1),
|
|
@@ -183,7 +188,7 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
183
188
|
{
|
|
184
189
|
function: forwardHostHeaderCfFunction,
|
|
185
190
|
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
186
|
-
}
|
|
191
|
+
},
|
|
187
192
|
],
|
|
188
193
|
};
|
|
189
194
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export {};
|
|
2
2
|
//# sourceMappingURL=source-code.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-code.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/source-code.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"source-code.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/source-code.ts"],"names":[],"mappings":""}
|
|
@@ -1,9 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
export function getFileContentsWithoutTypes(filePath) {
|
|
4
|
-
const source = fs.readFileSync(filePath, "utf8");
|
|
5
|
-
const result = ts.transpileModule(source, {
|
|
6
|
-
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 }
|
|
7
|
-
});
|
|
8
|
-
return result.outputText;
|
|
9
|
-
}
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infoxchange/make-it-so",
|
|
3
|
-
"version": "2.10.0-internal-testing-vdt-199-add-oidc-auth.
|
|
3
|
+
"version": "2.10.0-internal-testing-vdt-199-add-oidc-auth.3",
|
|
4
4
|
"description": "Makes deploying services to IX infra easy",
|
|
5
5
|
"repository": "github:infoxchange/make-it-so",
|
|
6
6
|
"type": "module",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
// Based off: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_kvs_jwt_verify_section.html
|
|
4
4
|
|
|
5
|
-
import crypto from
|
|
5
|
+
import crypto from "crypto";
|
|
6
6
|
// @ts-expect-error -- This library only exists in the CloudFront Functions runtime that this code runs in
|
|
7
|
-
import cf from
|
|
7
|
+
import cf from "cloudfront";
|
|
8
8
|
|
|
9
9
|
//Response when JWT is not valid.
|
|
10
10
|
// const response401 = {
|
|
@@ -14,154 +14,148 @@ import cf from 'cloudfront';
|
|
|
14
14
|
const response401 = {
|
|
15
15
|
statusCode: 302,
|
|
16
16
|
headers: {
|
|
17
|
-
location: {
|
|
17
|
+
location: { value: "/auth/oidc/authorize" },
|
|
18
18
|
},
|
|
19
|
-
}
|
|
19
|
+
};
|
|
20
20
|
|
|
21
21
|
// Remember to associate the KVS with your function before calling the const kvsKey = 'jwt.secret'.
|
|
22
22
|
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html
|
|
23
|
-
const kvsKey =
|
|
23
|
+
const kvsKey = "__placeholder-for-jwt-secret-key__";
|
|
24
24
|
// set to true to enable console logging
|
|
25
25
|
const loggingEnabled = true; // false;
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
function jwt_decode(token, key, noVerify, algorithm) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// check token
|
|
29
|
+
if (!token) {
|
|
30
|
+
throw new Error("No token supplied");
|
|
31
|
+
}
|
|
32
|
+
// check segments
|
|
33
|
+
const segments = token.split(".");
|
|
34
|
+
if (segments.length !== 3) {
|
|
35
|
+
throw new Error("Not enough or too many segments");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// All segment should be base64
|
|
39
|
+
const headerSeg = segments[0];
|
|
40
|
+
const payloadSeg = segments[1];
|
|
41
|
+
const signatureSeg = segments[2];
|
|
42
|
+
|
|
43
|
+
// base64 decode and parse JSON
|
|
44
|
+
const payload = JSON.parse(_base64urlDecode(payloadSeg));
|
|
45
|
+
|
|
46
|
+
if (!noVerify) {
|
|
47
|
+
const signingMethod = "sha256";
|
|
48
|
+
const signingType = "hmac";
|
|
49
|
+
|
|
50
|
+
// Verify signature. `sign` will return base64 string.
|
|
51
|
+
const signingInput = [headerSeg, payloadSeg].join(".");
|
|
52
|
+
|
|
53
|
+
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
|
|
54
|
+
throw new Error("Signature verification failed");
|
|
32
55
|
}
|
|
33
|
-
// check segments
|
|
34
|
-
const segments = token.split('.');
|
|
35
|
-
if (segments.length !== 3) {
|
|
36
|
-
throw new Error('Not enough or too many segments');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// All segment should be base64
|
|
40
|
-
const headerSeg = segments[0];
|
|
41
|
-
const payloadSeg = segments[1];
|
|
42
|
-
const signatureSeg = segments[2];
|
|
43
|
-
|
|
44
|
-
// base64 decode and parse JSON
|
|
45
|
-
const payload = JSON.parse(_base64urlDecode(payloadSeg));
|
|
46
|
-
|
|
47
|
-
if (!noVerify) {
|
|
48
|
-
const signingMethod = 'sha256';
|
|
49
|
-
const signingType = 'hmac';
|
|
50
|
-
|
|
51
|
-
// Verify signature. `sign` will return base64 string.
|
|
52
|
-
const signingInput = [headerSeg, payloadSeg].join('.');
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// According to the RFC, they should be in seconds.
|
|
60
|
-
if (payload.nbf && Date.now() < payload.nbf*1000) {
|
|
61
|
-
throw new Error('Token not yet active');
|
|
62
|
-
}
|
|
57
|
+
// Support for nbf and exp claims.
|
|
58
|
+
// According to the RFC, they should be in seconds.
|
|
59
|
+
if (payload.nbf && Date.now() < payload.nbf * 1000) {
|
|
60
|
+
throw new Error("Token not yet active");
|
|
61
|
+
}
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
63
|
+
if (payload.exp && Date.now() > payload.exp * 1000) {
|
|
64
|
+
throw new Error("Token expired");
|
|
67
65
|
}
|
|
66
|
+
}
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
return payload;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
//Function to ensure a constant time comparison to prevent
|
|
73
72
|
//timing side channels.
|
|
74
73
|
function _constantTimeEquals(a, b) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
if (a.length != b.length) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
xor |=
|
|
82
|
-
|
|
78
|
+
let xor = 0;
|
|
79
|
+
for (let i = 0; i < a.length; i++) {
|
|
80
|
+
xor |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
81
|
+
}
|
|
83
82
|
|
|
84
|
-
|
|
83
|
+
return 0 === xor;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
function _verify(input, key, method, type, signature) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
87
|
+
if (type === "hmac") {
|
|
88
|
+
return _constantTimeEquals(signature, _sign(input, key, method));
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error("Algorithm type not recognized");
|
|
91
|
+
}
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
function _sign(input, key, method) {
|
|
97
|
-
|
|
95
|
+
return crypto.createHmac(method, key).update(input).digest("base64url");
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
function _base64urlDecode(str) {
|
|
101
|
-
|
|
99
|
+
return Buffer.from(str, "base64url");
|
|
102
100
|
}
|
|
103
101
|
|
|
104
102
|
async function handler(event) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return request;
|
|
103
|
+
const request = event.request;
|
|
104
|
+
|
|
105
|
+
//Secret key used to verify JWT token.
|
|
106
|
+
//Update with your own key.
|
|
107
|
+
const secret_key = await getSecret();
|
|
108
|
+
|
|
109
|
+
if (!secret_key) {
|
|
110
|
+
return response401;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log("request");
|
|
114
|
+
console.log(request);
|
|
115
|
+
console.log(request.cookies);
|
|
116
|
+
console.log(request.cookies["auth-token"]);
|
|
117
|
+
console.log(Object.keys(request.cookies));
|
|
118
|
+
// console.logObject.keys(request.cookies))
|
|
119
|
+
|
|
120
|
+
// If no JWT token, then generate HTTP redirect 401 response.
|
|
121
|
+
if (!request.cookies["auth-token"]) {
|
|
122
|
+
log("Error: No JWT in the cookies");
|
|
123
|
+
return response401;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const jwtToken = request.cookies["auth-token"].value;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
jwt_decode(jwtToken, secret_key);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
log(e);
|
|
132
|
+
return response401;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//Remove the JWT from the query string if valid and return.
|
|
136
|
+
delete request.querystring.jwt;
|
|
137
|
+
log("Valid JWT token");
|
|
138
|
+
return request;
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
const publicKey =
|
|
145
|
-
`very-secret`
|
|
141
|
+
const publicKey = `very-secret`;
|
|
146
142
|
|
|
147
143
|
// get secret from key value store
|
|
148
144
|
async function getSecret() {
|
|
149
|
-
// console.log("auth key is:", publicKey)
|
|
150
|
-
// return publicKey
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
// console.log("auth key is:", publicKey)
|
|
146
|
+
// return publicKey
|
|
147
|
+
// initialize cloudfront kv store and get the key value
|
|
148
|
+
try {
|
|
149
|
+
const kvsHandle = cf.kvs();
|
|
150
|
+
return await kvsHandle.get(kvsKey);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
log(`Error reading value for key: ${kvsKey}, error: ${err}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
160
155
|
}
|
|
161
156
|
|
|
162
157
|
function log(message) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
if (loggingEnabled) {
|
|
159
|
+
console.log(message);
|
|
160
|
+
}
|
|
166
161
|
}
|
|
167
|
-
|
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import { AuthHandler, OidcAdapter } from "sst/node/auth";
|
|
2
2
|
import { Issuer } from "openid-client";
|
|
3
|
-
import jwt from "jsonwebtoken"
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
4
|
|
|
5
|
-
const oidcClientId = process.env.OIDC_CLIENT_ID
|
|
5
|
+
const oidcClientId = process.env.OIDC_CLIENT_ID;
|
|
6
6
|
if (!oidcClientId) {
|
|
7
7
|
throw new Error("OIDC_CLIENT_ID not set");
|
|
8
8
|
}
|
|
9
|
-
const oidcIssuerUrl = process.env.OIDC_ISSUER_URL
|
|
9
|
+
const oidcIssuerUrl = process.env.OIDC_ISSUER_URL;
|
|
10
10
|
if (!oidcIssuerUrl) {
|
|
11
11
|
throw new Error("OIDC_ISSUER_URL not set");
|
|
12
12
|
}
|
|
13
|
-
const oidcScope = process.env.OIDC_SCOPE
|
|
13
|
+
const oidcScope = process.env.OIDC_SCOPE;
|
|
14
14
|
if (!oidcScope) {
|
|
15
15
|
throw new Error("OIDC_SCOPE not set");
|
|
16
16
|
}
|
|
17
|
-
const jwtSecret = process.env.JWT_SECRET
|
|
17
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
18
18
|
if (!jwtSecret) {
|
|
19
19
|
throw new Error("JWT_SECRET not set");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const oidcIssuerConfigUrl = new URL(`${process.env.OIDC_ISSUER_URL?.replace(/\/$/, "")}/.well-known/openid-configuration`);
|
|
22
|
+
const oidcIssuerConfigUrl = new URL(
|
|
23
|
+
`${process.env.OIDC_ISSUER_URL?.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
24
|
+
);
|
|
26
25
|
|
|
27
26
|
export const handler = convertApiGatewayHandlerToCloudFrontHandler(
|
|
28
27
|
AuthHandler({
|
|
@@ -39,19 +38,19 @@ export const handler = convertApiGatewayHandlerToCloudFrontHandler(
|
|
|
39
38
|
// Payload to include in the token
|
|
40
39
|
const payload = {
|
|
41
40
|
userID: tokenset.claims().sub,
|
|
42
|
-
}
|
|
41
|
+
};
|
|
43
42
|
|
|
44
43
|
// Options (optional)
|
|
45
44
|
const options = {
|
|
46
|
-
algorithm:
|
|
47
|
-
expiresIn:
|
|
45
|
+
algorithm: "HS256",
|
|
46
|
+
expiresIn: "1h",
|
|
48
47
|
} as const;
|
|
49
48
|
|
|
50
49
|
// Create the token
|
|
51
50
|
const token = jwt.sign(payload, jwtSecret, options);
|
|
52
51
|
const expires = new Date(
|
|
53
52
|
// @ ts-ignore error in GH action
|
|
54
|
-
Date.now() +
|
|
53
|
+
Date.now() + 1000 * 60 * 60 * 24 * 7,
|
|
55
54
|
);
|
|
56
55
|
return {
|
|
57
56
|
statusCode: 302,
|
|
@@ -70,21 +69,21 @@ export const handler = convertApiGatewayHandlerToCloudFrontHandler(
|
|
|
70
69
|
// },
|
|
71
70
|
// });
|
|
72
71
|
},
|
|
73
|
-
})
|
|
72
|
+
}),
|
|
74
73
|
},
|
|
75
|
-
})
|
|
76
|
-
)
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
77
76
|
|
|
78
77
|
// @ts-expect-error - testing
|
|
79
78
|
function convertApiGatewayHandlerToCloudFrontHandler(callback) {
|
|
80
79
|
// @ts-expect-error - testing
|
|
81
80
|
return async function (event, context) {
|
|
82
81
|
// Used by AuthHandler to create callback url sent to oidc server
|
|
83
|
-
event.requestContext.domainName = event.headers[
|
|
84
|
-
console.log(
|
|
82
|
+
event.requestContext.domainName = event.headers["x-forwarded-host"];
|
|
83
|
+
console.log("----", event, context);
|
|
85
84
|
// console.log("event", event)
|
|
86
85
|
// console.log("context", context)
|
|
87
|
-
const response = await callback(event, context)
|
|
86
|
+
const response = await callback(event, context);
|
|
88
87
|
// if (response.cookies) {
|
|
89
88
|
// if (!response.headers) {
|
|
90
89
|
// response.headers = {}
|
|
@@ -93,6 +92,6 @@ function convertApiGatewayHandlerToCloudFrontHandler(callback) {
|
|
|
93
92
|
// }
|
|
94
93
|
// response.headers.location += "&cake=blar"
|
|
95
94
|
// response.headers.foo = "bar"
|
|
96
|
-
return response
|
|
97
|
-
}
|
|
95
|
+
return response;
|
|
96
|
+
};
|
|
98
97
|
}
|
|
@@ -2,13 +2,14 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import SecretsManager from "aws-cdk-lib/aws-secretsmanager";
|
|
3
3
|
import CloudFront from "aws-cdk-lib/aws-cloudfront";
|
|
4
4
|
import CDK from "aws-cdk-lib";
|
|
5
|
-
import CdkCustomResources from
|
|
6
|
-
import Lambda from
|
|
7
|
-
import { getFileContentsWithoutTypes } from "../../lib/utils/source-code.js";
|
|
5
|
+
import CdkCustomResources from "aws-cdk-lib/custom-resources";
|
|
6
|
+
import Lambda from "aws-cdk-lib/aws-lambda";
|
|
8
7
|
import * as SST from "sst/constructs";
|
|
9
|
-
import { Config as SSTInternalConfig } from "sst/config.js"
|
|
8
|
+
import { Config as SSTInternalConfig } from "sst/config.js";
|
|
10
9
|
import CloudFrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
11
10
|
import { BaseSiteCdkDistributionProps } from "sst/constructs/BaseSite.js";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "node:fs";
|
|
12
13
|
|
|
13
14
|
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
14
15
|
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
@@ -17,7 +18,6 @@ type Mutable<T> = {
|
|
|
17
18
|
-readonly [P in keyof T]: T[P];
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
|
|
21
21
|
type Props = {
|
|
22
22
|
oidcIssuerUrl: string;
|
|
23
23
|
oidcClientId: string;
|
|
@@ -38,27 +38,40 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
38
38
|
this.id = id;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
addToDistributionDefinition<
|
|
41
|
+
addToDistributionDefinition<
|
|
42
|
+
DistributionProps extends BaseSiteCdkDistributionProps,
|
|
43
|
+
>(
|
|
42
44
|
scope: ConstructScope,
|
|
43
|
-
{
|
|
45
|
+
{
|
|
46
|
+
distributionDefinition,
|
|
47
|
+
prefix = "/auth",
|
|
48
|
+
}: { distributionDefinition: Mutable<DistributionProps>; prefix?: string },
|
|
44
49
|
) {
|
|
45
|
-
console.log(
|
|
50
|
+
console.log(
|
|
51
|
+
"------",
|
|
52
|
+
import.meta.dirname,
|
|
53
|
+
import.meta.url,
|
|
54
|
+
import.meta.filename,
|
|
55
|
+
);
|
|
46
56
|
const updatedDistributionDefinition = { ...distributionDefinition };
|
|
47
|
-
const behaviourName = `${prefix.replace(/^\//g,
|
|
48
|
-
updatedDistributionDefinition.additionalBehaviors =
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const behaviourName = `${prefix.replace(/^\//g, "")}/*`;
|
|
58
|
+
updatedDistributionDefinition.additionalBehaviors =
|
|
59
|
+
updatedDistributionDefinition.additionalBehaviors
|
|
60
|
+
? { ...updatedDistributionDefinition.additionalBehaviors }
|
|
61
|
+
: {};
|
|
51
62
|
if (updatedDistributionDefinition.additionalBehaviors[behaviourName]) {
|
|
52
|
-
throw new Error(
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Behavior for prefix ${prefix} already exists in distribution definition`,
|
|
65
|
+
);
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
const jwtSecret = new SecretsManager.Secret(this, `${this.id}JwtSecret`, {
|
|
56
|
-
description:
|
|
69
|
+
description: "JWT Signing Secret",
|
|
57
70
|
generateSecretString: {
|
|
58
71
|
passwordLength: 32,
|
|
59
72
|
excludePunctuation: true,
|
|
60
73
|
includeSpace: false,
|
|
61
|
-
requireEachIncludedType: true
|
|
74
|
+
requireEachIncludedType: true,
|
|
62
75
|
},
|
|
63
76
|
// Secret is only used for sessions so it's safe to delete on stack removal
|
|
64
77
|
removalPolicy: CDK.RemovalPolicy.DESTROY,
|
|
@@ -67,116 +80,151 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
67
80
|
updatedDistributionDefinition.defaultBehavior = {
|
|
68
81
|
...updatedDistributionDefinition.defaultBehavior,
|
|
69
82
|
functionAssociations: [
|
|
70
|
-
...(updatedDistributionDefinition.defaultBehavior
|
|
71
|
-
|
|
83
|
+
...(updatedDistributionDefinition.defaultBehavior
|
|
84
|
+
?.functionAssociations || []),
|
|
85
|
+
this.getFunctionAssociation(scope, jwtSecret),
|
|
72
86
|
],
|
|
73
|
-
}
|
|
74
|
-
updatedDistributionDefinition.additionalBehaviors[behaviourName] =
|
|
87
|
+
};
|
|
88
|
+
updatedDistributionDefinition.additionalBehaviors[behaviourName] =
|
|
89
|
+
this.getAuthBehaviorOptions(scope, jwtSecret, prefix);
|
|
75
90
|
return updatedDistributionDefinition;
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
private getFunctionAssociation(
|
|
79
|
-
|
|
93
|
+
private getFunctionAssociation(
|
|
94
|
+
scope: ConstructScope,
|
|
95
|
+
jwtSecret: SecretsManager.Secret,
|
|
96
|
+
): CloudFront.FunctionAssociation {
|
|
97
|
+
const cfKeyValueStore = new CloudFront.KeyValueStore(
|
|
98
|
+
scope,
|
|
99
|
+
`${this.id}CFKeyValueStore`,
|
|
100
|
+
);
|
|
80
101
|
|
|
81
102
|
const kvStoreId = cfKeyValueStore.keyValueStoreId; // Your KV store ID
|
|
82
103
|
const key = "jwt-secret";
|
|
83
104
|
const kvsArn = `arn:aws:cloudfront::${CDK.Stack.of(this).account}:key-value-store/${kvStoreId}`;
|
|
84
105
|
|
|
85
106
|
// Updating the KVM requires a valid ETag to be provided in the IfMatch parameter so we first must fetch the ETag
|
|
86
|
-
const getEtag = new CdkCustomResources.AwsCustomResource(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
const getEtag = new CdkCustomResources.AwsCustomResource(
|
|
108
|
+
this,
|
|
109
|
+
`${this.id}GetKVStoreEtag`,
|
|
110
|
+
{
|
|
111
|
+
installLatestAwsSdk: false, // No real benefit in our case for the cost of a longer execution time
|
|
112
|
+
onUpdate: {
|
|
113
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
114
|
+
service: "@aws-sdk/client-cloudfront-keyvaluestore",
|
|
115
|
+
action: "describeKeyValueStore",
|
|
116
|
+
parameters: { KvsARN: kvsArn },
|
|
117
|
+
// We include a timestamp in the physicalResourceId to ensure we fetch the latest etag on every update
|
|
118
|
+
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(
|
|
119
|
+
`${kvStoreId}-etag-${Date.now()}`,
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
123
|
+
resources: [kvsArn],
|
|
124
|
+
}),
|
|
94
125
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}),
|
|
98
|
-
});
|
|
99
|
-
const etag = getEtag.getResponseField('ETag');
|
|
100
|
-
|
|
126
|
+
);
|
|
127
|
+
const etag = getEtag.getResponseField("ETag");
|
|
101
128
|
|
|
102
129
|
// An annoying limitation of CloudFormation is that it won't resolve dynamic references for secrets when
|
|
103
130
|
// used as a parameter to a custom resource. To get around this we manually resolve it with another custom
|
|
104
131
|
// resource. Note this won't result in the secret being exposed in CloudFormation templates but it will
|
|
105
132
|
// be visible in the CloudWatch logs of the custom resource lambda. In our case that is acceptable.
|
|
106
133
|
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/341
|
|
107
|
-
const secretValue = new CdkCustomResources.AwsCustomResource(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
const secretValue = new CdkCustomResources.AwsCustomResource(
|
|
135
|
+
this,
|
|
136
|
+
`${this.id}GetSecret`,
|
|
137
|
+
{
|
|
138
|
+
// There's no real benefit of fetching the latest sdk our case for the cost of a longer execution time
|
|
139
|
+
installLatestAwsSdk: false,
|
|
140
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
141
|
+
onUpdate: {
|
|
142
|
+
service: "@aws-sdk/client-secrets-manager",
|
|
143
|
+
action: "getSecretValue",
|
|
144
|
+
parameters: {
|
|
145
|
+
SecretId: jwtSecret.secretArn,
|
|
146
|
+
},
|
|
147
|
+
// We include a timestamp in the physicalResourceId to ensure we fetch the latest secret value on every update
|
|
148
|
+
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(
|
|
149
|
+
`${this.id}GetSecret-${Date.now()}`,
|
|
150
|
+
),
|
|
116
151
|
},
|
|
117
|
-
|
|
118
|
-
|
|
152
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
153
|
+
resources: [jwtSecret.secretArn],
|
|
154
|
+
}),
|
|
119
155
|
},
|
|
120
|
-
|
|
121
|
-
resources: [jwtSecret.secretArn],
|
|
122
|
-
}),
|
|
123
|
-
});
|
|
124
|
-
|
|
156
|
+
);
|
|
125
157
|
|
|
126
158
|
// Now we can actually update the KVS with the secret value
|
|
127
|
-
const putKeyValue = new CdkCustomResources.AwsCustomResource(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
const putKeyValue = new CdkCustomResources.AwsCustomResource(
|
|
160
|
+
this,
|
|
161
|
+
`${this.id}PutKeyValue`,
|
|
162
|
+
{
|
|
163
|
+
installLatestAwsSdk: false, // No real benefit in our case for the cost of a longer execution time
|
|
164
|
+
onUpdate: {
|
|
165
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
166
|
+
service: "@aws-sdk/client-cloudfront-keyvaluestore",
|
|
167
|
+
action: "putKey",
|
|
168
|
+
parameters: {
|
|
169
|
+
KvsARN: kvsArn,
|
|
170
|
+
Key: key,
|
|
171
|
+
Value: secretValue.getResponseField("SecretString"),
|
|
172
|
+
IfMatch: etag,
|
|
173
|
+
},
|
|
174
|
+
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(
|
|
175
|
+
`${kvStoreId}-${key}`,
|
|
176
|
+
),
|
|
137
177
|
},
|
|
138
|
-
|
|
178
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
179
|
+
resources: [kvsArn],
|
|
180
|
+
}),
|
|
139
181
|
},
|
|
140
|
-
|
|
141
|
-
resources: [kvsArn],
|
|
142
|
-
}),
|
|
143
|
-
});
|
|
182
|
+
);
|
|
144
183
|
|
|
145
184
|
// putKey in the @aws-sdk/client-cloudfront-keyvaluestore package requires @aws-sdk/signature-v4-crt to be imported
|
|
146
185
|
// as well. But AwsCustomResource doesn't give us direct access to the underlying Lambda function so we inject a
|
|
147
186
|
// NODE_OPTIONS env var to import on start. At some point AwsCustomResource will presumably switch to a later node
|
|
148
187
|
// version and we might need to update this to '--import=' instead of '--require='.
|
|
149
|
-
const fn = putKeyValue.node.findChild(
|
|
188
|
+
const fn = putKeyValue.node.findChild("Provider");
|
|
150
189
|
if (!(fn instanceof Lambda.SingletonFunction)) {
|
|
151
|
-
throw new Error(
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Could not find the underlying Lambda function of the AwsCustomResource",
|
|
192
|
+
);
|
|
152
193
|
}
|
|
153
|
-
fn.addEnvironment(
|
|
194
|
+
fn.addEnvironment("NODE_OPTIONS", "--require=@aws-sdk/signature-v4-crt");
|
|
154
195
|
|
|
155
|
-
const edgeFuncAuthCheck = new CloudFront.Function(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
const edgeFuncAuthCheck = new CloudFront.Function(
|
|
197
|
+
scope,
|
|
198
|
+
`${this.id}EdgeFunctionAuthCheck`,
|
|
199
|
+
{
|
|
200
|
+
code: CloudFront.FunctionCode.fromInline(
|
|
201
|
+
fs.readFileSync(path.join(import.meta.dirname, "auth-check.js"), "utf8").replace("__placeholder-for-jwt-secret-key__", key),
|
|
202
|
+
),
|
|
203
|
+
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
204
|
+
keyValueStore: cfKeyValueStore,
|
|
205
|
+
},
|
|
206
|
+
);
|
|
163
207
|
|
|
164
208
|
return {
|
|
165
209
|
function: edgeFuncAuthCheck,
|
|
166
210
|
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
167
|
-
}
|
|
211
|
+
};
|
|
168
212
|
}
|
|
169
213
|
|
|
170
|
-
private getAuthBehaviorOptions(
|
|
214
|
+
private getAuthBehaviorOptions(
|
|
215
|
+
scope: ConstructScope,
|
|
216
|
+
jwtSecret: SecretsManager.Secret,
|
|
217
|
+
prefix: string,
|
|
218
|
+
): CloudFront.BehaviorOptions {
|
|
171
219
|
const edgeFuncAuth = new SST.Function(scope, `${this.id}EdgeFunctionAuth`, {
|
|
172
220
|
runtime: "nodejs20.x",
|
|
173
|
-
handler: "
|
|
221
|
+
handler: path.join(import.meta.dirname, "auth-route.handler"),
|
|
174
222
|
environment: {
|
|
175
223
|
OIDC_ISSUER_URL: this.oidcIssuerUrl,
|
|
176
224
|
OIDC_CLIENT_ID: this.oidcClientId,
|
|
177
225
|
OIDC_SCOPE: this.oidcScope,
|
|
178
226
|
JWT_SECRET: jwtSecret.secretValue.toString(),
|
|
179
|
-
}
|
|
227
|
+
},
|
|
180
228
|
});
|
|
181
229
|
|
|
182
230
|
// edgeFuncAuth uses SST's AuthHandler construct which is normally run inside a lambda that's
|
|
@@ -192,39 +240,49 @@ export class CloudWatchOidcAuth extends Construct {
|
|
|
192
240
|
const edgeFuncAuthUrl = edgeFuncAuth.addFunctionUrl({
|
|
193
241
|
authType: Lambda.FunctionUrlAuthType.NONE,
|
|
194
242
|
});
|
|
195
|
-
const forwardHostHeaderCfFunction = new CloudFront.Function(
|
|
196
|
-
|
|
243
|
+
const forwardHostHeaderCfFunction = new CloudFront.Function(
|
|
244
|
+
scope,
|
|
245
|
+
`${this.id}ForwardHostHeaderFunction`,
|
|
246
|
+
{
|
|
247
|
+
code: CloudFront.FunctionCode.fromInline(`
|
|
197
248
|
function handler(event) {
|
|
198
249
|
const request = event.request;
|
|
199
250
|
request.headers["x-forwarded-host"] = { value: request.headers.host.value };
|
|
200
251
|
return request;
|
|
201
252
|
}
|
|
202
253
|
`),
|
|
203
|
-
|
|
204
|
-
|
|
254
|
+
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
255
|
+
},
|
|
256
|
+
);
|
|
205
257
|
|
|
206
258
|
return {
|
|
207
|
-
origin: new CloudFrontOrigins.HttpOrigin(
|
|
259
|
+
origin: new CloudFrontOrigins.HttpOrigin(
|
|
260
|
+
CDK.Fn.parseDomainName(edgeFuncAuthUrl.url),
|
|
261
|
+
),
|
|
208
262
|
allowedMethods: CloudFront.AllowedMethods.ALLOW_ALL,
|
|
209
|
-
cachePolicy: new CloudFront.CachePolicy(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
cachePolicy: new CloudFront.CachePolicy(
|
|
264
|
+
scope,
|
|
265
|
+
`${this.id}AllowAllCookiesPolicy`,
|
|
266
|
+
{
|
|
267
|
+
cachePolicyName: "AllowAllCookiesPolicy",
|
|
268
|
+
comment: "Cache policy that forwards all cookies",
|
|
269
|
+
defaultTtl: CDK.Duration.seconds(1),
|
|
270
|
+
minTtl: CDK.Duration.seconds(1),
|
|
271
|
+
maxTtl: CDK.Duration.seconds(1),
|
|
272
|
+
cookieBehavior: CloudFront.CacheCookieBehavior.all(),
|
|
273
|
+
headerBehavior:
|
|
274
|
+
CloudFront.CacheHeaderBehavior.allowList("X-Forwarded-Host"),
|
|
275
|
+
queryStringBehavior: CloudFront.CacheQueryStringBehavior.all(),
|
|
276
|
+
enableAcceptEncodingGzip: true,
|
|
277
|
+
enableAcceptEncodingBrotli: true,
|
|
278
|
+
},
|
|
279
|
+
),
|
|
221
280
|
functionAssociations: [
|
|
222
281
|
{
|
|
223
282
|
function: forwardHostHeaderCfFunction,
|
|
224
283
|
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
225
|
-
}
|
|
284
|
+
},
|
|
226
285
|
],
|
|
227
|
-
}
|
|
286
|
+
};
|
|
228
287
|
}
|
|
229
|
-
|
|
230
288
|
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
|
|
4
|
-
export function getFileContentsWithoutTypes(filePath: string): string {
|
|
5
|
-
const source = fs.readFileSync(filePath, "utf8");
|
|
6
|
-
|
|
7
|
-
const result = ts.transpileModule(source, {
|
|
8
|
-
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 }
|
|
9
|
-
});
|
|
10
|
-
return result.outputText
|
|
11
|
-
}
|