@infoxchange/make-it-so 2.9.1 → 2.10.0-internal-testing-vdt-199-add-oidc-auth.2
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 +9 -1
- package/commitlint.config.ts +14 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-check.d.ts +2 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-check.d.ts.map +1 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-check.js +135 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-route.d.ts +2 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-route.d.ts.map +1 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/auth-route.js +84 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/index.d.ts +27 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/index.d.ts.map +1 -0
- package/dist/cdk-constructs/CloudWatchOidcAuth/index.js +195 -0
- package/dist/cdk-constructs/index.d.ts +1 -0
- package/dist/cdk-constructs/index.d.ts.map +1 -1
- package/dist/cdk-constructs/index.js +1 -0
- package/dist/deployConfig.d.ts +35 -0
- package/dist/deployConfig.d.ts.map +1 -1
- package/dist/deployConfig.js +5 -3
- package/dist/lib/utils/source-code.d.ts +2 -0
- package/dist/lib/utils/source-code.d.ts.map +1 -0
- package/dist/lib/utils/source-code.js +12 -0
- package/package.json +3 -1
- package/src/cdk-constructs/CloudWatchOidcAuth/auth-check.ts +161 -0
- package/src/cdk-constructs/CloudWatchOidcAuth/auth-route.ts +97 -0
- package/src/cdk-constructs/CloudWatchOidcAuth/index.ts +290 -0
- package/src/cdk-constructs/index.ts +1 -0
- package/src/deployConfig.ts +25 -21
- package/src/lib/utils/source-code.ts +14 -0
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ npm --save-dev @infoxchange/make-it-so
|
|
|
13
13
|
yarn add --dev @infoxchange/make-it-so
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
|
|
16
18
|
## Features
|
|
17
19
|
|
|
18
20
|
### deployConfig
|
|
@@ -20,7 +22,9 @@ yarn add --dev @infoxchange/make-it-so
|
|
|
20
22
|
The IX pipeline provides certain information about the deployment currently in progress via environment variables. deployConfig gives you a friendly (and typed) way to access these details.
|
|
21
23
|
|
|
22
24
|
```typescript
|
|
23
|
-
import deployConfig
|
|
25
|
+
import deployConfig, {
|
|
26
|
+
getDeployConfig,
|
|
27
|
+
} from "@infoxchange/make-it-so/deployConfig";
|
|
24
28
|
|
|
25
29
|
if (deployConfig.isIxDeploy) {
|
|
26
30
|
console.log(
|
|
@@ -29,6 +33,10 @@ if (deployConfig.isIxDeploy) {
|
|
|
29
33
|
} else {
|
|
30
34
|
console.log(`Not deploying via the IX deploy pipeline`);
|
|
31
35
|
}
|
|
36
|
+
|
|
37
|
+
// Will return the same object but calculated when the function is run rather than when imported. Useful if any IX
|
|
38
|
+
// deployment related environment variables are changed at runtime.
|
|
39
|
+
console.log(getDeployConfig());
|
|
32
40
|
```
|
|
33
41
|
|
|
34
42
|
<details>
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-check.d.ts","sourceRoot":"","sources":["../../../src/cdk-constructs/CloudWatchOidcAuth/auth-check.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// Based off: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_kvs_jwt_verify_section.html
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
// @ts-expect-error -- This library only exists in the CloudFront Functions runtime that this code runs in
|
|
6
|
+
import cf from "cloudfront";
|
|
7
|
+
//Response when JWT is not valid.
|
|
8
|
+
// const response401 = {
|
|
9
|
+
// statusCode: 401,
|
|
10
|
+
// statusDescription: 'Unauthorized'
|
|
11
|
+
// };
|
|
12
|
+
const response401 = {
|
|
13
|
+
statusCode: 302,
|
|
14
|
+
headers: {
|
|
15
|
+
location: { value: "/auth/oidc/authorize" },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
// Remember to associate the KVS with your function before calling the const kvsKey = 'jwt.secret'.
|
|
19
|
+
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html
|
|
20
|
+
const kvsKey = "__placeholder-for-jwt-secret-key__";
|
|
21
|
+
// set to true to enable console logging
|
|
22
|
+
const loggingEnabled = true; // false;
|
|
23
|
+
function jwt_decode(token, key, noVerify, algorithm) {
|
|
24
|
+
// check token
|
|
25
|
+
if (!token) {
|
|
26
|
+
throw new Error("No token supplied");
|
|
27
|
+
}
|
|
28
|
+
// check segments
|
|
29
|
+
const segments = token.split(".");
|
|
30
|
+
if (segments.length !== 3) {
|
|
31
|
+
throw new Error("Not enough or too many segments");
|
|
32
|
+
}
|
|
33
|
+
// All segment should be base64
|
|
34
|
+
const headerSeg = segments[0];
|
|
35
|
+
const payloadSeg = segments[1];
|
|
36
|
+
const signatureSeg = segments[2];
|
|
37
|
+
// base64 decode and parse JSON
|
|
38
|
+
const payload = JSON.parse(_base64urlDecode(payloadSeg));
|
|
39
|
+
if (!noVerify) {
|
|
40
|
+
const signingMethod = "sha256";
|
|
41
|
+
const signingType = "hmac";
|
|
42
|
+
// Verify signature. `sign` will return base64 string.
|
|
43
|
+
const signingInput = [headerSeg, payloadSeg].join(".");
|
|
44
|
+
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
|
|
45
|
+
throw new Error("Signature verification failed");
|
|
46
|
+
}
|
|
47
|
+
// Support for nbf and exp claims.
|
|
48
|
+
// According to the RFC, they should be in seconds.
|
|
49
|
+
if (payload.nbf && Date.now() < payload.nbf * 1000) {
|
|
50
|
+
throw new Error("Token not yet active");
|
|
51
|
+
}
|
|
52
|
+
if (payload.exp && Date.now() > payload.exp * 1000) {
|
|
53
|
+
throw new Error("Token expired");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
//Function to ensure a constant time comparison to prevent
|
|
59
|
+
//timing side channels.
|
|
60
|
+
function _constantTimeEquals(a, b) {
|
|
61
|
+
if (a.length != b.length) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
let xor = 0;
|
|
65
|
+
for (let i = 0; i < a.length; i++) {
|
|
66
|
+
xor |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
67
|
+
}
|
|
68
|
+
return 0 === xor;
|
|
69
|
+
}
|
|
70
|
+
function _verify(input, key, method, type, signature) {
|
|
71
|
+
if (type === "hmac") {
|
|
72
|
+
return _constantTimeEquals(signature, _sign(input, key, method));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
throw new Error("Algorithm type not recognized");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function _sign(input, key, method) {
|
|
79
|
+
return crypto.createHmac(method, key).update(input).digest("base64url");
|
|
80
|
+
}
|
|
81
|
+
function _base64urlDecode(str) {
|
|
82
|
+
return Buffer.from(str, "base64url");
|
|
83
|
+
}
|
|
84
|
+
async function handler(event) {
|
|
85
|
+
const request = event.request;
|
|
86
|
+
//Secret key used to verify JWT token.
|
|
87
|
+
//Update with your own key.
|
|
88
|
+
const secret_key = await getSecret();
|
|
89
|
+
if (!secret_key) {
|
|
90
|
+
return response401;
|
|
91
|
+
}
|
|
92
|
+
console.log("request");
|
|
93
|
+
console.log(request);
|
|
94
|
+
console.log(request.cookies);
|
|
95
|
+
console.log(request.cookies["auth-token"]);
|
|
96
|
+
console.log(Object.keys(request.cookies));
|
|
97
|
+
// console.logObject.keys(request.cookies))
|
|
98
|
+
// If no JWT token, then generate HTTP redirect 401 response.
|
|
99
|
+
if (!request.cookies["auth-token"]) {
|
|
100
|
+
log("Error: No JWT in the cookies");
|
|
101
|
+
return response401;
|
|
102
|
+
}
|
|
103
|
+
const jwtToken = request.cookies["auth-token"].value;
|
|
104
|
+
try {
|
|
105
|
+
jwt_decode(jwtToken, secret_key);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
log(e);
|
|
109
|
+
return response401;
|
|
110
|
+
}
|
|
111
|
+
//Remove the JWT from the query string if valid and return.
|
|
112
|
+
delete request.querystring.jwt;
|
|
113
|
+
log("Valid JWT token");
|
|
114
|
+
return request;
|
|
115
|
+
}
|
|
116
|
+
const publicKey = `very-secret`;
|
|
117
|
+
// get secret from key value store
|
|
118
|
+
async function getSecret() {
|
|
119
|
+
// console.log("auth key is:", publicKey)
|
|
120
|
+
// return publicKey
|
|
121
|
+
// initialize cloudfront kv store and get the key value
|
|
122
|
+
try {
|
|
123
|
+
const kvsHandle = cf.kvs();
|
|
124
|
+
return await kvsHandle.get(kvsKey);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
log(`Error reading value for key: ${kvsKey}, error: ${err}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function log(message) {
|
|
132
|
+
if (loggingEnabled) {
|
|
133
|
+
console.log(message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AuthHandler, OidcAdapter } from "sst/node/auth";
|
|
2
|
+
import { Issuer } from "openid-client";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
const oidcClientId = process.env.OIDC_CLIENT_ID;
|
|
5
|
+
if (!oidcClientId) {
|
|
6
|
+
throw new Error("OIDC_CLIENT_ID not set");
|
|
7
|
+
}
|
|
8
|
+
const oidcIssuerUrl = process.env.OIDC_ISSUER_URL;
|
|
9
|
+
if (!oidcIssuerUrl) {
|
|
10
|
+
throw new Error("OIDC_ISSUER_URL not set");
|
|
11
|
+
}
|
|
12
|
+
const oidcScope = process.env.OIDC_SCOPE;
|
|
13
|
+
if (!oidcScope) {
|
|
14
|
+
throw new Error("OIDC_SCOPE not set");
|
|
15
|
+
}
|
|
16
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
17
|
+
if (!jwtSecret) {
|
|
18
|
+
throw new Error("JWT_SECRET not set");
|
|
19
|
+
}
|
|
20
|
+
const oidcIssuerConfigUrl = new URL(`${process.env.OIDC_ISSUER_URL?.replace(/\/$/, "")}/.well-known/openid-configuration`);
|
|
21
|
+
export const handler = convertApiGatewayHandlerToCloudFrontHandler(AuthHandler({
|
|
22
|
+
providers: {
|
|
23
|
+
oidc: OidcAdapter({
|
|
24
|
+
issuer: await Issuer.discover(oidcIssuerConfigUrl.href),
|
|
25
|
+
clientID: oidcClientId,
|
|
26
|
+
scope: oidcScope,
|
|
27
|
+
onSuccess: async (tokenset) => {
|
|
28
|
+
console.log("tokenset", tokenset, tokenset.claims());
|
|
29
|
+
console.log("Config.jwtSecret:", jwtSecret);
|
|
30
|
+
// Payload to include in the token
|
|
31
|
+
const payload = {
|
|
32
|
+
userID: tokenset.claims().sub,
|
|
33
|
+
};
|
|
34
|
+
// Options (optional)
|
|
35
|
+
const options = {
|
|
36
|
+
algorithm: "HS256",
|
|
37
|
+
expiresIn: "1h",
|
|
38
|
+
};
|
|
39
|
+
// Create the token
|
|
40
|
+
const token = jwt.sign(payload, jwtSecret, options);
|
|
41
|
+
const expires = new Date(
|
|
42
|
+
// @ ts-ignore error in GH action
|
|
43
|
+
Date.now() + 1000 * 60 * 60 * 24 * 7);
|
|
44
|
+
return {
|
|
45
|
+
statusCode: 302,
|
|
46
|
+
headers: {
|
|
47
|
+
location: "/",
|
|
48
|
+
},
|
|
49
|
+
cookies: [
|
|
50
|
+
`auth-token=${token}; HttpOnly; SameSite=None; Secure; Path=/; Expires=${expires}`,
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
// return Session.cookie({
|
|
54
|
+
// redirect: "https://openidconnect.net/callback",
|
|
55
|
+
// type: "public",
|
|
56
|
+
// properties: {
|
|
57
|
+
// userID: tokenset.claims().sub,
|
|
58
|
+
// },
|
|
59
|
+
// });
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
}));
|
|
64
|
+
// @ts-expect-error - testing
|
|
65
|
+
function convertApiGatewayHandlerToCloudFrontHandler(callback) {
|
|
66
|
+
// @ts-expect-error - testing
|
|
67
|
+
return async function (event, context) {
|
|
68
|
+
// Used by AuthHandler to create callback url sent to oidc server
|
|
69
|
+
event.requestContext.domainName = event.headers["x-forwarded-host"];
|
|
70
|
+
console.log("----", event, context);
|
|
71
|
+
// console.log("event", event)
|
|
72
|
+
// console.log("context", context)
|
|
73
|
+
const response = await callback(event, context);
|
|
74
|
+
// if (response.cookies) {
|
|
75
|
+
// if (!response.headers) {
|
|
76
|
+
// response.headers = {}
|
|
77
|
+
// }
|
|
78
|
+
// response.headers["set-cookie"] = response.cookies
|
|
79
|
+
// }
|
|
80
|
+
// response.headers.location += "&cake=blar"
|
|
81
|
+
// response.headers.foo = "bar"
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import { BaseSiteCdkDistributionProps } from "sst/constructs/BaseSite.js";
|
|
3
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
4
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
5
|
+
type Mutable<T> = {
|
|
6
|
+
-readonly [P in keyof T]: T[P];
|
|
7
|
+
};
|
|
8
|
+
type Props = {
|
|
9
|
+
oidcIssuerUrl: string;
|
|
10
|
+
oidcClientId: string;
|
|
11
|
+
oidcScope: string;
|
|
12
|
+
};
|
|
13
|
+
export declare class CloudWatchOidcAuth extends Construct {
|
|
14
|
+
readonly oidcIssuerUrl: string;
|
|
15
|
+
readonly oidcClientId: string;
|
|
16
|
+
readonly oidcScope: string;
|
|
17
|
+
readonly id: string;
|
|
18
|
+
constructor(scope: ConstructScope, id: ConstructId, props: Props);
|
|
19
|
+
addToDistributionDefinition<DistributionProps extends BaseSiteCdkDistributionProps>(scope: ConstructScope, { distributionDefinition, prefix, }: {
|
|
20
|
+
distributionDefinition: Mutable<DistributionProps>;
|
|
21
|
+
prefix?: string;
|
|
22
|
+
}): Mutable<DistributionProps>;
|
|
23
|
+
private getFunctionAssociation;
|
|
24
|
+
private getAuthBehaviorOptions;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;AAUvC,OAAO,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAG1E,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;IA2H9B,OAAO,CAAC,sBAAsB;CA0E/B"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import SecretsManager from "aws-cdk-lib/aws-secretsmanager";
|
|
3
|
+
import CloudFront from "aws-cdk-lib/aws-cloudfront";
|
|
4
|
+
import CDK from "aws-cdk-lib";
|
|
5
|
+
import CdkCustomResources from "aws-cdk-lib/custom-resources";
|
|
6
|
+
import Lambda from "aws-cdk-lib/aws-lambda";
|
|
7
|
+
import { getFileContentsWithoutTypes } from "../../lib/utils/source-code.js";
|
|
8
|
+
import * as SST from "sst/constructs";
|
|
9
|
+
import { Config as SSTInternalConfig } from "sst/config.js";
|
|
10
|
+
import CloudFrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
export class CloudWatchOidcAuth extends Construct {
|
|
13
|
+
oidcIssuerUrl;
|
|
14
|
+
oidcClientId;
|
|
15
|
+
oidcScope;
|
|
16
|
+
id;
|
|
17
|
+
constructor(scope, id, props) {
|
|
18
|
+
super(scope, id);
|
|
19
|
+
this.oidcIssuerUrl = props.oidcIssuerUrl;
|
|
20
|
+
this.oidcClientId = props.oidcClientId;
|
|
21
|
+
this.oidcScope = props.oidcScope;
|
|
22
|
+
this.id = id;
|
|
23
|
+
}
|
|
24
|
+
addToDistributionDefinition(scope, { distributionDefinition, prefix = "/auth", }) {
|
|
25
|
+
console.log("------", import.meta.dirname, import.meta.url, import.meta.filename);
|
|
26
|
+
const updatedDistributionDefinition = { ...distributionDefinition };
|
|
27
|
+
const behaviourName = `${prefix.replace(/^\//g, "")}/*`;
|
|
28
|
+
updatedDistributionDefinition.additionalBehaviors =
|
|
29
|
+
updatedDistributionDefinition.additionalBehaviors
|
|
30
|
+
? { ...updatedDistributionDefinition.additionalBehaviors }
|
|
31
|
+
: {};
|
|
32
|
+
if (updatedDistributionDefinition.additionalBehaviors[behaviourName]) {
|
|
33
|
+
throw new Error(`Behavior for prefix ${prefix} already exists in distribution definition`);
|
|
34
|
+
}
|
|
35
|
+
const jwtSecret = new SecretsManager.Secret(this, `${this.id}JwtSecret`, {
|
|
36
|
+
description: "JWT Signing Secret",
|
|
37
|
+
generateSecretString: {
|
|
38
|
+
passwordLength: 32,
|
|
39
|
+
excludePunctuation: true,
|
|
40
|
+
includeSpace: false,
|
|
41
|
+
requireEachIncludedType: true,
|
|
42
|
+
},
|
|
43
|
+
// Secret is only used for sessions so it's safe to delete on stack removal
|
|
44
|
+
removalPolicy: CDK.RemovalPolicy.DESTROY,
|
|
45
|
+
});
|
|
46
|
+
updatedDistributionDefinition.defaultBehavior = {
|
|
47
|
+
...updatedDistributionDefinition.defaultBehavior,
|
|
48
|
+
functionAssociations: [
|
|
49
|
+
...(updatedDistributionDefinition.defaultBehavior
|
|
50
|
+
?.functionAssociations || []),
|
|
51
|
+
this.getFunctionAssociation(scope, jwtSecret),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
updatedDistributionDefinition.additionalBehaviors[behaviourName] =
|
|
55
|
+
this.getAuthBehaviorOptions(scope, jwtSecret, prefix);
|
|
56
|
+
return updatedDistributionDefinition;
|
|
57
|
+
}
|
|
58
|
+
getFunctionAssociation(scope, jwtSecret) {
|
|
59
|
+
const cfKeyValueStore = new CloudFront.KeyValueStore(scope, `${this.id}CFKeyValueStore`);
|
|
60
|
+
const kvStoreId = cfKeyValueStore.keyValueStoreId; // Your KV store ID
|
|
61
|
+
const key = "jwt-secret";
|
|
62
|
+
const kvsArn = `arn:aws:cloudfront::${CDK.Stack.of(this).account}:key-value-store/${kvStoreId}`;
|
|
63
|
+
// Updating the KVM requires a valid ETag to be provided in the IfMatch parameter so we first must fetch the ETag
|
|
64
|
+
const getEtag = new CdkCustomResources.AwsCustomResource(this, `${this.id}GetKVStoreEtag`, {
|
|
65
|
+
installLatestAwsSdk: false, // No real benefit in our case for the cost of a longer execution time
|
|
66
|
+
onUpdate: {
|
|
67
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
68
|
+
service: "@aws-sdk/client-cloudfront-keyvaluestore",
|
|
69
|
+
action: "describeKeyValueStore",
|
|
70
|
+
parameters: { KvsARN: kvsArn },
|
|
71
|
+
// We include a timestamp in the physicalResourceId to ensure we fetch the latest etag on every update
|
|
72
|
+
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(`${kvStoreId}-etag-${Date.now()}`),
|
|
73
|
+
},
|
|
74
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
75
|
+
resources: [kvsArn],
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
const etag = getEtag.getResponseField("ETag");
|
|
79
|
+
// An annoying limitation of CloudFormation is that it won't resolve dynamic references for secrets when
|
|
80
|
+
// used as a parameter to a custom resource. To get around this we manually resolve it with another custom
|
|
81
|
+
// resource. Note this won't result in the secret being exposed in CloudFormation templates but it will
|
|
82
|
+
// be visible in the CloudWatch logs of the custom resource lambda. In our case that is acceptable.
|
|
83
|
+
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/341
|
|
84
|
+
const secretValue = new CdkCustomResources.AwsCustomResource(this, `${this.id}GetSecret`, {
|
|
85
|
+
// There's no real benefit of fetching the latest sdk our case for the cost of a longer execution time
|
|
86
|
+
installLatestAwsSdk: false,
|
|
87
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
88
|
+
onUpdate: {
|
|
89
|
+
service: "@aws-sdk/client-secrets-manager",
|
|
90
|
+
action: "getSecretValue",
|
|
91
|
+
parameters: {
|
|
92
|
+
SecretId: jwtSecret.secretArn,
|
|
93
|
+
},
|
|
94
|
+
// We include a timestamp in the physicalResourceId to ensure we fetch the latest secret value on every update
|
|
95
|
+
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(`${this.id}GetSecret-${Date.now()}`),
|
|
96
|
+
},
|
|
97
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
98
|
+
resources: [jwtSecret.secretArn],
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
// Now we can actually update the KVS with the secret value
|
|
102
|
+
const putKeyValue = new CdkCustomResources.AwsCustomResource(this, `${this.id}PutKeyValue`, {
|
|
103
|
+
installLatestAwsSdk: false, // No real benefit in our case for the cost of a longer execution time
|
|
104
|
+
onUpdate: {
|
|
105
|
+
// Since there's no onCreate, onUpdate will be called for CREATE events
|
|
106
|
+
service: "@aws-sdk/client-cloudfront-keyvaluestore",
|
|
107
|
+
action: "putKey",
|
|
108
|
+
parameters: {
|
|
109
|
+
KvsARN: kvsArn,
|
|
110
|
+
Key: key,
|
|
111
|
+
Value: secretValue.getResponseField("SecretString"),
|
|
112
|
+
IfMatch: etag,
|
|
113
|
+
},
|
|
114
|
+
physicalResourceId: CdkCustomResources.PhysicalResourceId.of(`${kvStoreId}-${key}`),
|
|
115
|
+
},
|
|
116
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
117
|
+
resources: [kvsArn],
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
// putKey in the @aws-sdk/client-cloudfront-keyvaluestore package requires @aws-sdk/signature-v4-crt to be imported
|
|
121
|
+
// as well. But AwsCustomResource doesn't give us direct access to the underlying Lambda function so we inject a
|
|
122
|
+
// NODE_OPTIONS env var to import on start. At some point AwsCustomResource will presumably switch to a later node
|
|
123
|
+
// version and we might need to update this to '--import=' instead of '--require='.
|
|
124
|
+
const fn = putKeyValue.node.findChild("Provider");
|
|
125
|
+
if (!(fn instanceof Lambda.SingletonFunction)) {
|
|
126
|
+
throw new Error("Could not find the underlying Lambda function of the AwsCustomResource");
|
|
127
|
+
}
|
|
128
|
+
fn.addEnvironment("NODE_OPTIONS", "--require=@aws-sdk/signature-v4-crt");
|
|
129
|
+
const edgeFuncAuthCheck = new CloudFront.Function(scope, `${this.id}EdgeFunctionAuthCheck`, {
|
|
130
|
+
code: CloudFront.FunctionCode.fromInline(getFileContentsWithoutTypes(path.join(import.meta.dirname, "auth-check.ts")).replace("__placeholder-for-jwt-secret-key__", key)),
|
|
131
|
+
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
132
|
+
keyValueStore: cfKeyValueStore,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
function: edgeFuncAuthCheck,
|
|
136
|
+
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
getAuthBehaviorOptions(scope, jwtSecret, prefix) {
|
|
140
|
+
const edgeFuncAuth = new SST.Function(scope, `${this.id}EdgeFunctionAuth`, {
|
|
141
|
+
runtime: "nodejs20.x",
|
|
142
|
+
handler: path.join(import.meta.dirname, "auth-route.handler"),
|
|
143
|
+
environment: {
|
|
144
|
+
OIDC_ISSUER_URL: this.oidcIssuerUrl,
|
|
145
|
+
OIDC_CLIENT_ID: this.oidcClientId,
|
|
146
|
+
OIDC_SCOPE: this.oidcScope,
|
|
147
|
+
JWT_SECRET: jwtSecret.secretValue.toString(),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
// edgeFuncAuth uses SST's AuthHandler construct which is normally run inside a lambda that's
|
|
151
|
+
// created by SST's Auth construct. AuthHandler expects certain environment variables to be set
|
|
152
|
+
// by the Auth construct so we have to set them ourselves here to keep it happy.
|
|
153
|
+
const envVarName = SSTInternalConfig.envFor({
|
|
154
|
+
type: "Auth",
|
|
155
|
+
id: "id", // It seems like the env var will still be found no matter what this value is
|
|
156
|
+
prop: "prefix",
|
|
157
|
+
});
|
|
158
|
+
edgeFuncAuth.addEnvironment(envVarName, prefix);
|
|
159
|
+
const edgeFuncAuthUrl = edgeFuncAuth.addFunctionUrl({
|
|
160
|
+
authType: Lambda.FunctionUrlAuthType.NONE,
|
|
161
|
+
});
|
|
162
|
+
const forwardHostHeaderCfFunction = new CloudFront.Function(scope, `${this.id}ForwardHostHeaderFunction`, {
|
|
163
|
+
code: CloudFront.FunctionCode.fromInline(`
|
|
164
|
+
function handler(event) {
|
|
165
|
+
const request = event.request;
|
|
166
|
+
request.headers["x-forwarded-host"] = { value: request.headers.host.value };
|
|
167
|
+
return request;
|
|
168
|
+
}
|
|
169
|
+
`),
|
|
170
|
+
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
origin: new CloudFrontOrigins.HttpOrigin(CDK.Fn.parseDomainName(edgeFuncAuthUrl.url)),
|
|
174
|
+
allowedMethods: CloudFront.AllowedMethods.ALLOW_ALL,
|
|
175
|
+
cachePolicy: new CloudFront.CachePolicy(scope, `${this.id}AllowAllCookiesPolicy`, {
|
|
176
|
+
cachePolicyName: "AllowAllCookiesPolicy",
|
|
177
|
+
comment: "Cache policy that forwards all cookies",
|
|
178
|
+
defaultTtl: CDK.Duration.seconds(1),
|
|
179
|
+
minTtl: CDK.Duration.seconds(1),
|
|
180
|
+
maxTtl: CDK.Duration.seconds(1),
|
|
181
|
+
cookieBehavior: CloudFront.CacheCookieBehavior.all(),
|
|
182
|
+
headerBehavior: CloudFront.CacheHeaderBehavior.allowList("X-Forwarded-Host"),
|
|
183
|
+
queryStringBehavior: CloudFront.CacheQueryStringBehavior.all(),
|
|
184
|
+
enableAcceptEncodingGzip: true,
|
|
185
|
+
enableAcceptEncodingBrotli: true,
|
|
186
|
+
}),
|
|
187
|
+
functionAssociations: [
|
|
188
|
+
{
|
|
189
|
+
function: forwardHostHeaderCfFunction,
|
|
190
|
+
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdk-constructs/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC"}
|
package/dist/deployConfig.d.ts
CHANGED
|
@@ -34,4 +34,39 @@ declare const _default: {
|
|
|
34
34
|
smtpPort?: number | undefined;
|
|
35
35
|
};
|
|
36
36
|
export default _default;
|
|
37
|
+
export declare const getDeployConfig: () => {
|
|
38
|
+
isIxDeploy: true;
|
|
39
|
+
appName: string;
|
|
40
|
+
environment: "dev" | "test" | "uat" | "prod";
|
|
41
|
+
workloadGroup: "ds" | "srs";
|
|
42
|
+
primaryAwsRegion: "ap-southeast-2";
|
|
43
|
+
siteDomains: string[];
|
|
44
|
+
siteDomainAliases: string[];
|
|
45
|
+
isInternalApp: boolean;
|
|
46
|
+
deploymentType: "docker" | "serverless";
|
|
47
|
+
sourceCommitRef: string;
|
|
48
|
+
sourceCommitHash: string;
|
|
49
|
+
deployTriggeredBy: string;
|
|
50
|
+
smtpHost: string;
|
|
51
|
+
smtpPort: number;
|
|
52
|
+
clamAVUrl: string;
|
|
53
|
+
vpcHttpProxy: string;
|
|
54
|
+
} | {
|
|
55
|
+
isIxDeploy: false;
|
|
56
|
+
appName: string;
|
|
57
|
+
environment: string;
|
|
58
|
+
workloadGroup: string;
|
|
59
|
+
primaryAwsRegion: string;
|
|
60
|
+
siteDomains: string[];
|
|
61
|
+
siteDomainAliases: string[];
|
|
62
|
+
deploymentType: string;
|
|
63
|
+
sourceCommitRef: string;
|
|
64
|
+
sourceCommitHash: string;
|
|
65
|
+
deployTriggeredBy: string;
|
|
66
|
+
smtpHost: string;
|
|
67
|
+
clamAVUrl: string;
|
|
68
|
+
vpcHttpProxy: string;
|
|
69
|
+
isInternalApp?: boolean | undefined;
|
|
70
|
+
smtpPort?: number | undefined;
|
|
71
|
+
};
|
|
37
72
|
//# sourceMappingURL=deployConfig.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployConfig.d.ts","sourceRoot":"","sources":["../src/deployConfig.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"deployConfig.d.ts","sourceRoot":"","sources":["../src/deployConfig.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,wBAA0C;AAG1C,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAmC,CAAC"}
|
package/dist/deployConfig.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
const
|
|
2
|
+
const getEnvVars = () => ({
|
|
3
3
|
isIxDeploy: process.env.IX_DEPLOYMENT?.toLowerCase() === "true", // This needs to start as a bool for the discriminated union
|
|
4
4
|
appName: process.env.IX_APP_NAME ?? "",
|
|
5
5
|
environment: process.env.IX_ENVIRONMENT ?? "",
|
|
@@ -16,7 +16,7 @@ const envVars = {
|
|
|
16
16
|
smtpPort: process.env.SMTP_PORT ?? "",
|
|
17
17
|
clamAVUrl: process.env.CLAMAV_URL ?? "",
|
|
18
18
|
vpcHttpProxy: process.env.VPC_HTTP_PROXY ?? "",
|
|
19
|
-
};
|
|
19
|
+
});
|
|
20
20
|
const ixDeployConfigSchema = z
|
|
21
21
|
.object({
|
|
22
22
|
isIxDeploy: z.literal(true),
|
|
@@ -73,4 +73,6 @@ const schema = z.discriminatedUnion("isIxDeploy", [
|
|
|
73
73
|
ixDeployConfigSchema,
|
|
74
74
|
nonIxDeployConfigSchema,
|
|
75
75
|
]);
|
|
76
|
-
export default schema.parse(
|
|
76
|
+
export default schema.parse(getEnvVars());
|
|
77
|
+
// process.env values can change at runtime so we provide a way to re-parse the config as needed
|
|
78
|
+
export const getDeployConfig = () => schema.parse(getEnvVars());
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-code.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/source-code.ts"],"names":[],"mappings":"AAGA,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUpE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
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: {
|
|
7
|
+
module: ts.ModuleKind.ESNext,
|
|
8
|
+
target: ts.ScriptTarget.ES2020,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
return result.outputText;
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infoxchange/make-it-so",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0-internal-testing-vdt-199-add-oidc-auth.2",
|
|
4
4
|
"description": "Makes deploying services to IX infra easy",
|
|
5
5
|
"repository": "github:infoxchange/make-it-so",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@commitlint/prompt-cli": "^19.3.1",
|
|
31
31
|
"@eslint/js": "^9.3.0",
|
|
32
32
|
"@tsconfig/node21": "^21.0.3",
|
|
33
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
33
34
|
"aws-cdk-lib": "2.142.1",
|
|
34
35
|
"constructs": "^10.3.0",
|
|
35
36
|
"eslint": "^8.57.0",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"sst": "^2.0.0"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
54
|
+
"jsonwebtoken": "^9.0.2",
|
|
53
55
|
"zod": "^3.24.2"
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
// Based off: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_kvs_jwt_verify_section.html
|
|
4
|
+
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
// @ts-expect-error -- This library only exists in the CloudFront Functions runtime that this code runs in
|
|
7
|
+
import cf from "cloudfront";
|
|
8
|
+
|
|
9
|
+
//Response when JWT is not valid.
|
|
10
|
+
// const response401 = {
|
|
11
|
+
// statusCode: 401,
|
|
12
|
+
// statusDescription: 'Unauthorized'
|
|
13
|
+
// };
|
|
14
|
+
const response401 = {
|
|
15
|
+
statusCode: 302,
|
|
16
|
+
headers: {
|
|
17
|
+
location: { value: "/auth/oidc/authorize" },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Remember to associate the KVS with your function before calling the const kvsKey = 'jwt.secret'.
|
|
22
|
+
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/kvs-with-functions-associate.html
|
|
23
|
+
const kvsKey = "__placeholder-for-jwt-secret-key__";
|
|
24
|
+
// set to true to enable console logging
|
|
25
|
+
const loggingEnabled = true; // false;
|
|
26
|
+
|
|
27
|
+
function jwt_decode(token, key, noVerify, algorithm) {
|
|
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");
|
|
55
|
+
}
|
|
56
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
if (payload.exp && Date.now() > payload.exp * 1000) {
|
|
64
|
+
throw new Error("Token expired");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return payload;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//Function to ensure a constant time comparison to prevent
|
|
72
|
+
//timing side channels.
|
|
73
|
+
function _constantTimeEquals(a, b) {
|
|
74
|
+
if (a.length != b.length) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let xor = 0;
|
|
79
|
+
for (let i = 0; i < a.length; i++) {
|
|
80
|
+
xor |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return 0 === xor;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _verify(input, key, method, type, signature) {
|
|
87
|
+
if (type === "hmac") {
|
|
88
|
+
return _constantTimeEquals(signature, _sign(input, key, method));
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error("Algorithm type not recognized");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _sign(input, key, method) {
|
|
95
|
+
return crypto.createHmac(method, key).update(input).digest("base64url");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _base64urlDecode(str) {
|
|
99
|
+
return Buffer.from(str, "base64url");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handler(event) {
|
|
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;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const publicKey = `very-secret`;
|
|
142
|
+
|
|
143
|
+
// get secret from key value store
|
|
144
|
+
async function getSecret() {
|
|
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
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function log(message) {
|
|
158
|
+
if (loggingEnabled) {
|
|
159
|
+
console.log(message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { AuthHandler, OidcAdapter } from "sst/node/auth";
|
|
2
|
+
import { Issuer } from "openid-client";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
|
|
5
|
+
const oidcClientId = process.env.OIDC_CLIENT_ID;
|
|
6
|
+
if (!oidcClientId) {
|
|
7
|
+
throw new Error("OIDC_CLIENT_ID not set");
|
|
8
|
+
}
|
|
9
|
+
const oidcIssuerUrl = process.env.OIDC_ISSUER_URL;
|
|
10
|
+
if (!oidcIssuerUrl) {
|
|
11
|
+
throw new Error("OIDC_ISSUER_URL not set");
|
|
12
|
+
}
|
|
13
|
+
const oidcScope = process.env.OIDC_SCOPE;
|
|
14
|
+
if (!oidcScope) {
|
|
15
|
+
throw new Error("OIDC_SCOPE not set");
|
|
16
|
+
}
|
|
17
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
18
|
+
if (!jwtSecret) {
|
|
19
|
+
throw new Error("JWT_SECRET not set");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const oidcIssuerConfigUrl = new URL(
|
|
23
|
+
`${process.env.OIDC_ISSUER_URL?.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const handler = convertApiGatewayHandlerToCloudFrontHandler(
|
|
27
|
+
AuthHandler({
|
|
28
|
+
providers: {
|
|
29
|
+
oidc: OidcAdapter({
|
|
30
|
+
issuer: await Issuer.discover(oidcIssuerConfigUrl.href),
|
|
31
|
+
clientID: oidcClientId,
|
|
32
|
+
scope: oidcScope,
|
|
33
|
+
onSuccess: async (tokenset) => {
|
|
34
|
+
console.log("tokenset", tokenset, tokenset.claims());
|
|
35
|
+
|
|
36
|
+
console.log("Config.jwtSecret:", jwtSecret);
|
|
37
|
+
|
|
38
|
+
// Payload to include in the token
|
|
39
|
+
const payload = {
|
|
40
|
+
userID: tokenset.claims().sub,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Options (optional)
|
|
44
|
+
const options = {
|
|
45
|
+
algorithm: "HS256",
|
|
46
|
+
expiresIn: "1h",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
// Create the token
|
|
50
|
+
const token = jwt.sign(payload, jwtSecret, options);
|
|
51
|
+
const expires = new Date(
|
|
52
|
+
// @ ts-ignore error in GH action
|
|
53
|
+
Date.now() + 1000 * 60 * 60 * 24 * 7,
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
statusCode: 302,
|
|
57
|
+
headers: {
|
|
58
|
+
location: "/",
|
|
59
|
+
},
|
|
60
|
+
cookies: [
|
|
61
|
+
`auth-token=${token}; HttpOnly; SameSite=None; Secure; Path=/; Expires=${expires}`,
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
// return Session.cookie({
|
|
65
|
+
// redirect: "https://openidconnect.net/callback",
|
|
66
|
+
// type: "public",
|
|
67
|
+
// properties: {
|
|
68
|
+
// userID: tokenset.claims().sub,
|
|
69
|
+
// },
|
|
70
|
+
// });
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// @ts-expect-error - testing
|
|
78
|
+
function convertApiGatewayHandlerToCloudFrontHandler(callback) {
|
|
79
|
+
// @ts-expect-error - testing
|
|
80
|
+
return async function (event, context) {
|
|
81
|
+
// Used by AuthHandler to create callback url sent to oidc server
|
|
82
|
+
event.requestContext.domainName = event.headers["x-forwarded-host"];
|
|
83
|
+
console.log("----", event, context);
|
|
84
|
+
// console.log("event", event)
|
|
85
|
+
// console.log("context", context)
|
|
86
|
+
const response = await callback(event, context);
|
|
87
|
+
// if (response.cookies) {
|
|
88
|
+
// if (!response.headers) {
|
|
89
|
+
// response.headers = {}
|
|
90
|
+
// }
|
|
91
|
+
// response.headers["set-cookie"] = response.cookies
|
|
92
|
+
// }
|
|
93
|
+
// response.headers.location += "&cake=blar"
|
|
94
|
+
// response.headers.foo = "bar"
|
|
95
|
+
return response;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
import SecretsManager from "aws-cdk-lib/aws-secretsmanager";
|
|
3
|
+
import CloudFront from "aws-cdk-lib/aws-cloudfront";
|
|
4
|
+
import CDK from "aws-cdk-lib";
|
|
5
|
+
import CdkCustomResources from "aws-cdk-lib/custom-resources";
|
|
6
|
+
import Lambda from "aws-cdk-lib/aws-lambda";
|
|
7
|
+
import { getFileContentsWithoutTypes } from "../../lib/utils/source-code.js";
|
|
8
|
+
import * as SST from "sst/constructs";
|
|
9
|
+
import { Config as SSTInternalConfig } from "sst/config.js";
|
|
10
|
+
import CloudFrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";
|
|
11
|
+
import { BaseSiteCdkDistributionProps } from "sst/constructs/BaseSite.js";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
type ConstructScope = ConstructorParameters<typeof Construct>[0];
|
|
15
|
+
type ConstructId = ConstructorParameters<typeof Construct>[1];
|
|
16
|
+
|
|
17
|
+
type Mutable<T> = {
|
|
18
|
+
-readonly [P in keyof T]: T[P];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Props = {
|
|
22
|
+
oidcIssuerUrl: string;
|
|
23
|
+
oidcClientId: string;
|
|
24
|
+
oidcScope: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class CloudWatchOidcAuth extends Construct {
|
|
28
|
+
readonly oidcIssuerUrl: string;
|
|
29
|
+
readonly oidcClientId: string;
|
|
30
|
+
readonly oidcScope: string;
|
|
31
|
+
readonly id: string;
|
|
32
|
+
|
|
33
|
+
constructor(scope: ConstructScope, id: ConstructId, props: Props) {
|
|
34
|
+
super(scope, id);
|
|
35
|
+
this.oidcIssuerUrl = props.oidcIssuerUrl;
|
|
36
|
+
this.oidcClientId = props.oidcClientId;
|
|
37
|
+
this.oidcScope = props.oidcScope;
|
|
38
|
+
this.id = id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addToDistributionDefinition<
|
|
42
|
+
DistributionProps extends BaseSiteCdkDistributionProps,
|
|
43
|
+
>(
|
|
44
|
+
scope: ConstructScope,
|
|
45
|
+
{
|
|
46
|
+
distributionDefinition,
|
|
47
|
+
prefix = "/auth",
|
|
48
|
+
}: { distributionDefinition: Mutable<DistributionProps>; prefix?: string },
|
|
49
|
+
) {
|
|
50
|
+
console.log(
|
|
51
|
+
"------",
|
|
52
|
+
import.meta.dirname,
|
|
53
|
+
import.meta.url,
|
|
54
|
+
import.meta.filename,
|
|
55
|
+
);
|
|
56
|
+
const updatedDistributionDefinition = { ...distributionDefinition };
|
|
57
|
+
const behaviourName = `${prefix.replace(/^\//g, "")}/*`;
|
|
58
|
+
updatedDistributionDefinition.additionalBehaviors =
|
|
59
|
+
updatedDistributionDefinition.additionalBehaviors
|
|
60
|
+
? { ...updatedDistributionDefinition.additionalBehaviors }
|
|
61
|
+
: {};
|
|
62
|
+
if (updatedDistributionDefinition.additionalBehaviors[behaviourName]) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Behavior for prefix ${prefix} already exists in distribution definition`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const jwtSecret = new SecretsManager.Secret(this, `${this.id}JwtSecret`, {
|
|
69
|
+
description: "JWT Signing Secret",
|
|
70
|
+
generateSecretString: {
|
|
71
|
+
passwordLength: 32,
|
|
72
|
+
excludePunctuation: true,
|
|
73
|
+
includeSpace: false,
|
|
74
|
+
requireEachIncludedType: true,
|
|
75
|
+
},
|
|
76
|
+
// Secret is only used for sessions so it's safe to delete on stack removal
|
|
77
|
+
removalPolicy: CDK.RemovalPolicy.DESTROY,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
updatedDistributionDefinition.defaultBehavior = {
|
|
81
|
+
...updatedDistributionDefinition.defaultBehavior,
|
|
82
|
+
functionAssociations: [
|
|
83
|
+
...(updatedDistributionDefinition.defaultBehavior
|
|
84
|
+
?.functionAssociations || []),
|
|
85
|
+
this.getFunctionAssociation(scope, jwtSecret),
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
updatedDistributionDefinition.additionalBehaviors[behaviourName] =
|
|
89
|
+
this.getAuthBehaviorOptions(scope, jwtSecret, prefix);
|
|
90
|
+
return updatedDistributionDefinition;
|
|
91
|
+
}
|
|
92
|
+
|
|
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
|
+
);
|
|
101
|
+
|
|
102
|
+
const kvStoreId = cfKeyValueStore.keyValueStoreId; // Your KV store ID
|
|
103
|
+
const key = "jwt-secret";
|
|
104
|
+
const kvsArn = `arn:aws:cloudfront::${CDK.Stack.of(this).account}:key-value-store/${kvStoreId}`;
|
|
105
|
+
|
|
106
|
+
// Updating the KVM requires a valid ETag to be provided in the IfMatch parameter so we first must fetch the ETag
|
|
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
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
const etag = getEtag.getResponseField("ETag");
|
|
128
|
+
|
|
129
|
+
// An annoying limitation of CloudFormation is that it won't resolve dynamic references for secrets when
|
|
130
|
+
// used as a parameter to a custom resource. To get around this we manually resolve it with another custom
|
|
131
|
+
// resource. Note this won't result in the secret being exposed in CloudFormation templates but it will
|
|
132
|
+
// be visible in the CloudWatch logs of the custom resource lambda. In our case that is acceptable.
|
|
133
|
+
// https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/341
|
|
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
|
+
),
|
|
151
|
+
},
|
|
152
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
153
|
+
resources: [jwtSecret.secretArn],
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Now we can actually update the KVS with the secret value
|
|
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
|
+
),
|
|
177
|
+
},
|
|
178
|
+
policy: CdkCustomResources.AwsCustomResourcePolicy.fromSdkCalls({
|
|
179
|
+
resources: [kvsArn],
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// putKey in the @aws-sdk/client-cloudfront-keyvaluestore package requires @aws-sdk/signature-v4-crt to be imported
|
|
185
|
+
// as well. But AwsCustomResource doesn't give us direct access to the underlying Lambda function so we inject a
|
|
186
|
+
// NODE_OPTIONS env var to import on start. At some point AwsCustomResource will presumably switch to a later node
|
|
187
|
+
// version and we might need to update this to '--import=' instead of '--require='.
|
|
188
|
+
const fn = putKeyValue.node.findChild("Provider");
|
|
189
|
+
if (!(fn instanceof Lambda.SingletonFunction)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Could not find the underlying Lambda function of the AwsCustomResource",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
fn.addEnvironment("NODE_OPTIONS", "--require=@aws-sdk/signature-v4-crt");
|
|
195
|
+
|
|
196
|
+
const edgeFuncAuthCheck = new CloudFront.Function(
|
|
197
|
+
scope,
|
|
198
|
+
`${this.id}EdgeFunctionAuthCheck`,
|
|
199
|
+
{
|
|
200
|
+
code: CloudFront.FunctionCode.fromInline(
|
|
201
|
+
getFileContentsWithoutTypes(
|
|
202
|
+
path.join(import.meta.dirname, "auth-check.ts"),
|
|
203
|
+
).replace("__placeholder-for-jwt-secret-key__", key),
|
|
204
|
+
),
|
|
205
|
+
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
206
|
+
keyValueStore: cfKeyValueStore,
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
function: edgeFuncAuthCheck,
|
|
212
|
+
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private getAuthBehaviorOptions(
|
|
217
|
+
scope: ConstructScope,
|
|
218
|
+
jwtSecret: SecretsManager.Secret,
|
|
219
|
+
prefix: string,
|
|
220
|
+
): CloudFront.BehaviorOptions {
|
|
221
|
+
const edgeFuncAuth = new SST.Function(scope, `${this.id}EdgeFunctionAuth`, {
|
|
222
|
+
runtime: "nodejs20.x",
|
|
223
|
+
handler: path.join(import.meta.dirname, "auth-route.handler"),
|
|
224
|
+
environment: {
|
|
225
|
+
OIDC_ISSUER_URL: this.oidcIssuerUrl,
|
|
226
|
+
OIDC_CLIENT_ID: this.oidcClientId,
|
|
227
|
+
OIDC_SCOPE: this.oidcScope,
|
|
228
|
+
JWT_SECRET: jwtSecret.secretValue.toString(),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// edgeFuncAuth uses SST's AuthHandler construct which is normally run inside a lambda that's
|
|
233
|
+
// created by SST's Auth construct. AuthHandler expects certain environment variables to be set
|
|
234
|
+
// by the Auth construct so we have to set them ourselves here to keep it happy.
|
|
235
|
+
const envVarName = SSTInternalConfig.envFor({
|
|
236
|
+
type: "Auth",
|
|
237
|
+
id: "id", // It seems like the env var will still be found no matter what this value is
|
|
238
|
+
prop: "prefix",
|
|
239
|
+
});
|
|
240
|
+
edgeFuncAuth.addEnvironment(envVarName, prefix);
|
|
241
|
+
|
|
242
|
+
const edgeFuncAuthUrl = edgeFuncAuth.addFunctionUrl({
|
|
243
|
+
authType: Lambda.FunctionUrlAuthType.NONE,
|
|
244
|
+
});
|
|
245
|
+
const forwardHostHeaderCfFunction = new CloudFront.Function(
|
|
246
|
+
scope,
|
|
247
|
+
`${this.id}ForwardHostHeaderFunction`,
|
|
248
|
+
{
|
|
249
|
+
code: CloudFront.FunctionCode.fromInline(`
|
|
250
|
+
function handler(event) {
|
|
251
|
+
const request = event.request;
|
|
252
|
+
request.headers["x-forwarded-host"] = { value: request.headers.host.value };
|
|
253
|
+
return request;
|
|
254
|
+
}
|
|
255
|
+
`),
|
|
256
|
+
runtime: CloudFront.FunctionRuntime.JS_2_0,
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
origin: new CloudFrontOrigins.HttpOrigin(
|
|
262
|
+
CDK.Fn.parseDomainName(edgeFuncAuthUrl.url),
|
|
263
|
+
),
|
|
264
|
+
allowedMethods: CloudFront.AllowedMethods.ALLOW_ALL,
|
|
265
|
+
cachePolicy: new CloudFront.CachePolicy(
|
|
266
|
+
scope,
|
|
267
|
+
`${this.id}AllowAllCookiesPolicy`,
|
|
268
|
+
{
|
|
269
|
+
cachePolicyName: "AllowAllCookiesPolicy",
|
|
270
|
+
comment: "Cache policy that forwards all cookies",
|
|
271
|
+
defaultTtl: CDK.Duration.seconds(1),
|
|
272
|
+
minTtl: CDK.Duration.seconds(1),
|
|
273
|
+
maxTtl: CDK.Duration.seconds(1),
|
|
274
|
+
cookieBehavior: CloudFront.CacheCookieBehavior.all(),
|
|
275
|
+
headerBehavior:
|
|
276
|
+
CloudFront.CacheHeaderBehavior.allowList("X-Forwarded-Host"),
|
|
277
|
+
queryStringBehavior: CloudFront.CacheQueryStringBehavior.all(),
|
|
278
|
+
enableAcceptEncodingGzip: true,
|
|
279
|
+
enableAcceptEncodingBrotli: true,
|
|
280
|
+
},
|
|
281
|
+
),
|
|
282
|
+
functionAssociations: [
|
|
283
|
+
{
|
|
284
|
+
function: forwardHostHeaderCfFunction,
|
|
285
|
+
eventType: CloudFront.FunctionEventType.VIEWER_REQUEST,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/deployConfig.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
const getEnvVars = () =>
|
|
4
|
+
({
|
|
5
|
+
isIxDeploy: process.env.IX_DEPLOYMENT?.toLowerCase() === "true", // This needs to start as a bool for the discriminated union
|
|
6
|
+
appName: process.env.IX_APP_NAME ?? "",
|
|
7
|
+
environment: process.env.IX_ENVIRONMENT ?? "",
|
|
8
|
+
workloadGroup: process.env.IX_WORKLOAD_GROUP ?? "",
|
|
9
|
+
primaryAwsRegion: process.env.IX_PRIMARY_AWS_REGION ?? "",
|
|
10
|
+
siteDomains: process.env.IX_SITE_DOMAINS ?? "",
|
|
11
|
+
siteDomainAliases: process.env.IX_SITE_DOMAIN_ALIASES ?? "",
|
|
12
|
+
isInternalApp: process.env.IX_INTERNAL_APP ?? "",
|
|
13
|
+
deploymentType: process.env.IX_DEPLOYMENT_TYPE ?? "",
|
|
14
|
+
sourceCommitRef: process.env.IX_SOURCE_COMMIT_REF ?? "",
|
|
15
|
+
sourceCommitHash: process.env.IX_SOURCE_COMMIT_HASH ?? "",
|
|
16
|
+
deployTriggeredBy: process.env.IX_DEPLOY_TRIGGERED_BY ?? "",
|
|
17
|
+
smtpHost: process.env.SMTP_HOST ?? "",
|
|
18
|
+
smtpPort: process.env.SMTP_PORT ?? "",
|
|
19
|
+
clamAVUrl: process.env.CLAMAV_URL ?? "",
|
|
20
|
+
vpcHttpProxy: process.env.VPC_HTTP_PROXY ?? "",
|
|
21
|
+
}) satisfies Record<string, string | boolean>;
|
|
21
22
|
|
|
22
23
|
const ixDeployConfigSchema = z
|
|
23
24
|
.object({
|
|
@@ -41,7 +42,7 @@ const ixDeployConfigSchema = z
|
|
|
41
42
|
smtpPort: z.coerce.number().int(),
|
|
42
43
|
clamAVUrl: z.string().url(),
|
|
43
44
|
vpcHttpProxy: z.string().url(),
|
|
44
|
-
} satisfies Record<keyof typeof
|
|
45
|
+
} satisfies Record<keyof ReturnType<typeof getEnvVars>, unknown>)
|
|
45
46
|
.strip();
|
|
46
47
|
|
|
47
48
|
const nonIxDeployConfigSchema = z
|
|
@@ -72,7 +73,7 @@ const nonIxDeployConfigSchema = z
|
|
|
72
73
|
),
|
|
73
74
|
clamAVUrl: z.string(),
|
|
74
75
|
vpcHttpProxy: z.string(),
|
|
75
|
-
} satisfies Record<keyof typeof
|
|
76
|
+
} satisfies Record<keyof ReturnType<typeof getEnvVars>, unknown>)
|
|
76
77
|
.strip();
|
|
77
78
|
|
|
78
79
|
const schema = z.discriminatedUnion("isIxDeploy", [
|
|
@@ -80,4 +81,7 @@ const schema = z.discriminatedUnion("isIxDeploy", [
|
|
|
80
81
|
nonIxDeployConfigSchema,
|
|
81
82
|
]);
|
|
82
83
|
|
|
83
|
-
export default schema.parse(
|
|
84
|
+
export default schema.parse(getEnvVars());
|
|
85
|
+
|
|
86
|
+
// process.env values can change at runtime so we provide a way to re-parse the config as needed
|
|
87
|
+
export const getDeployConfig = () => schema.parse(getEnvVars());
|
|
@@ -0,0 +1,14 @@
|
|
|
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: {
|
|
9
|
+
module: ts.ModuleKind.ESNext,
|
|
10
|
+
target: ts.ScriptTarget.ES2020,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
return result.outputText;
|
|
14
|
+
}
|