├── .husky
├── .gitignore
└── commit-msg
├── file-mock.js
├── .eslintignore
├── .gitignore
├── .prettierrc
├── src
├── index.ts
├── handlers
│ ├── error-page
│ │ ├── html.d.ts
│ │ └── template.html
│ ├── http-headers.ts
│ ├── generate-secret.ts
│ ├── util
│ │ ├── base64.ts
│ │ ├── logger.ts
│ │ ├── nonce.ts
│ │ ├── config.ts
│ │ ├── axios.ts
│ │ ├── jwt.ts
│ │ ├── cloudfront.ts
│ │ └── cookies.ts
│ ├── sign-out.ts
│ ├── check-auth.test.ts
│ ├── refresh-auth.ts
│ ├── check-auth.ts
│ └── parse-auth.ts
├── client-secret.ts
├── client-update.ts
├── generate-secret.ts
├── index.test.ts
├── lambdas.ts
├── cloudfront-auth.ts
└── __snapshots__
│ └── index.test.ts.snap
├── example
├── cdk.context.json
├── .gitignore
├── website
│ └── index.html
├── tsconfig.json
├── package.json
├── lib
│ ├── auth-lambdas-stack.ts
│ ├── cognito-user.ts
│ └── main-stack.ts
├── bin
│ └── example.ts
├── README.md
├── cdk.json
└── package-lock.json
├── commitlint.config.js
├── tsconfig.eslint.json
├── tsconfig.json
├── .gitattributes
├── .editorconfig
├── jest.config.js
├── tsconfig.base.json
├── renovate.json
├── .eslintrc
├── .github
└── workflows
│ └── build.yaml
├── LICENSE
├── webpack.config.js
├── README.md
└── package.json
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/file-mock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-file-stub"
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist/
2 | /lib/
3 | /example/
4 | /cdk.out/
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | node_modules/
3 | /dist/
4 | /lib/
5 | /*.tgz
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./cloudfront-auth"
2 | export * from "./lambdas"
3 |
--------------------------------------------------------------------------------
/example/cdk.context.json:
--------------------------------------------------------------------------------
1 | {
2 | "acknowledged-issue-numbers": [
3 | 19836
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"],
3 | }
4 |
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "exclude": ["node_modules"]
4 | }
5 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit "$1"
5 |
--------------------------------------------------------------------------------
/src/handlers/error-page/html.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.html" {
2 | const content: string
3 | export default content
4 | }
5 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | *.js
2 | !jest.config.js
3 | *.d.ts
4 | node_modules
5 |
6 | # CDK asset staging directory
7 | .cdk.staging
8 | cdk.out
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "include": ["src"],
4 | "exclude": ["src/**/*.test.ts", "src/handlers/*.ts"]
5 | }
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/en/github/administering-a-repository/customizing-how-changed-files-appear-on-github
2 | **/__snapshots__/** linguist-generated=true
3 |
--------------------------------------------------------------------------------
/example/website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello world
5 |
6 |
7 | Hello world
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 2
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // Override values in CDK stacks during tests.
2 | process.env.IS_SNAPSHOT = "true"
3 |
4 | module.exports = {
5 | testMatch: ["**/*.test.ts"],
6 | transform: {
7 | "^.+\\.tsx?$": "ts-jest",
8 | },
9 | moduleNameMapper: {
10 | "\\.html$": "/file-mock.js",
11 | },
12 | }
13 |
--------------------------------------------------------------------------------
/src/handlers/http-headers.ts:
--------------------------------------------------------------------------------
1 | import { createResponseHandler } from "./util/cloudfront"
2 |
3 | // Headers are added in the response handler.
4 | export const handler = createResponseHandler(
5 | // eslint-disable-next-line @typescript-eslint/require-await
6 | async (config, event) => event.Records[0].cf.response,
7 | )
8 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "esModuleInterop": true,
5 | "inlineSourceMap": true,
6 | "inlineSources": true,
7 | "lib": ["es2018", "es2019"],
8 | "module": "commonjs",
9 | "outDir": "./lib",
10 | "strict": true,
11 | "strictPropertyInitialization": false,
12 | "target": "ES2018"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true,
4 | "declaration": false,
5 | "inlineSourceMap": true,
6 | "inlineSources": true,
7 | "lib": ["es2018", "es2019"],
8 | "module": "commonjs",
9 | "strict": true,
10 | "strictPropertyInitialization": false,
11 | "target": "ES2018"
12 | },
13 | "exclude": ["node_modules", "cdk.out"]
14 | }
15 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.0.0-development",
4 | "scripts": {
5 | "cdk": "cdk",
6 | "test": "cdk synth"
7 | },
8 | "devDependencies": {
9 | "@types/node": "18.19.130",
10 | "aws-cdk": "2.1033.0",
11 | "ts-node": "10.9.2",
12 | "typescript": "4.9.5"
13 | },
14 | "dependencies": {
15 | "@henrist/cdk-cloudfront-auth": "^2",
16 | "aws-cdk-lib": "^2",
17 | "source-map-support": "^0.5"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/handlers/error-page/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 | ${title}
12 | ${message}
13 |
14 | ${details} [log region: ${region}]
15 |
16 |
17 | ${linkText}
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/lib/auth-lambdas-stack.ts:
--------------------------------------------------------------------------------
1 | import { AuthLambdas } from "@henrist/cdk-cloudfront-auth"
2 | import * as cdk from "aws-cdk-lib"
3 | import { Construct } from "constructs"
4 |
5 | export class AuthLambdasStack extends cdk.Stack {
6 | readonly authLambdas: AuthLambdas
7 |
8 | constructor(scope: Construct, id: string, props?: cdk.StackProps) {
9 | super(scope, id, props)
10 |
11 | this.authLambdas = new AuthLambdas(this, "AuthLambdas", {
12 | regions: ["eu-west-1"],
13 | })
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["github>henrist/renovate-config:library"],
4 | "automerge": true,
5 | "automergeType": "branch",
6 | "packageRules": [
7 | {
8 | "description": "Create release for package updates that is part of the bundled code",
9 | "packageNames": ["aws-sdk", "axios", "cookie", "jsonwebtoken", "jwks-rsa", "typescript", "webpack"],
10 | "semanticCommitType": "fix",
11 | "matchFiles": ["package.json"]
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:@typescript-eslint/recommended",
4 | "plugin:@typescript-eslint/recommended-requiring-type-checking",
5 | "prettier",
6 | "plugin:prettier/recommended"
7 | ],
8 | "overrides": [
9 | {
10 | "files": ["*.ts"]
11 | }
12 | ],
13 | "parserOptions": {
14 | "project": "./tsconfig.eslint.json",
15 | "sourceType": "module"
16 | },
17 | "rules": {
18 | "@typescript-eslint/no-non-null-assertion": "off",
19 | "@typescript-eslint/no-unused-vars": "error"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example/bin/example.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import * as cdk from "aws-cdk-lib"
3 | import "source-map-support/register"
4 | import { AuthLambdasStack } from "../lib/auth-lambdas-stack"
5 | import { MainStack } from "../lib/main-stack"
6 |
7 | const app = new cdk.App()
8 |
9 | const authLambdasStack = new AuthLambdasStack(app, "auth-lambdas", {
10 | env: {
11 | region: "us-east-1",
12 | },
13 | stackName: "cdk-cloudfront-auth-example-auth-lambdas",
14 | })
15 |
16 | new MainStack(app, "main", {
17 | env: {
18 | region: "eu-west-1",
19 | },
20 | stackName: "cdk-cloudfront-auth-example-main",
21 | authLambdas: authLambdasStack.authLambdas,
22 | })
23 |
--------------------------------------------------------------------------------
/src/handlers/generate-secret.ts:
--------------------------------------------------------------------------------
1 | import { randomBytes } from "crypto"
2 |
3 | type OnEventHandler = (event: {
4 | PhysicalResourceId?: string
5 | RequestType: "Create" | "Update" | "Delete"
6 | }) => Promise<{
7 | PhysicalResourceId?: string
8 | Data?: Record
9 | }>
10 |
11 | // eslint-disable-next-line @typescript-eslint/require-await
12 | export const handler: OnEventHandler = async (event) => {
13 | switch (event.RequestType) {
14 | case "Delete":
15 | return {
16 | PhysicalResourceId: event.PhysicalResourceId,
17 | }
18 |
19 | case "Create":
20 | case "Update":
21 | return {
22 | PhysicalResourceId: "generate-secret",
23 | Data: {
24 | Value: randomBytes(16).toString("hex"),
25 | },
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/handlers/util/base64.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Functions to translate base64-encoded strings, so they can be used:
3 |
4 | - in URL's without needing additional encoding
5 | - in OAuth2 PKCE verifier
6 | - in cookies (to be on the safe side, as = + / are in fact valid characters in cookies)
7 | */
8 |
9 | /**
10 | * Use this on a base64-encoded string to translate = + / into replacement characters.
11 | */
12 | export function safeBase64Stringify(value: string): string {
13 | return value.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
14 | }
15 |
16 | /**
17 | * Decode a Base64 value that is run through safeBase64Stringify to the actual string.
18 | */
19 | export function decodeSafeBase64(value: string): string {
20 | const desafed = value.replace(/-/g, "+").replace(/_/g, "/")
21 | return Buffer.from(desafed, "base64").toString()
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/build.yaml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push]
3 |
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v3
9 | - uses: actions/setup-node@v3
10 | with:
11 | node-version: '18'
12 | - run: npm ci
13 | - run: npm run lint
14 | - run: npm run test
15 |
16 | # example project
17 | - run: npm pack
18 | - run: npm ci
19 | working-directory: example
20 | - run: npm install --no-save ../henrist-cdk-cloudfront-auth-0.0.0-development.tgz
21 | working-directory: example
22 | - run: npm run test
23 | working-directory: example
24 |
25 | - run: npm run semantic-release
26 | if: github.ref == 'refs/heads/master'
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
30 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # example
2 |
3 | This provides a simple CDK application demonstrating the
4 | usage of the construct.
5 |
6 | It also works as a way to spin up a simple application for manual
7 | integration testing.
8 |
9 | ## Manual testing
10 |
11 | ```bash
12 | cd ..
13 | npm pack
14 | cd example
15 | npm install --no-save ../henrist-cdk-cloudfront-auth-0.0.0-development.tgz
16 | # must be logged in to aws for next command
17 | npx cdk deploy --all
18 | ```
19 |
20 | See link to test page in outputs.
21 |
22 | A user example@example.com with password example is created.
23 |
24 | Add or remove user from `test` group to test authorization.
25 |
26 | ## Cleanup
27 |
28 | ```bash
29 | # (modify the next bucket name first, see deploy output)
30 | aws s3 rm --recursive s3://cdk-cloudfront-auth-example-main-bucket83908e77-wc5jf6w82bqb
31 | npx cdk destroy main
32 | # wait so that CloudFront frees up the lambdas
33 | npx cdk destroy auth-lambdas
34 | ```
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Henrik Steen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/handlers/sign-out.ts:
--------------------------------------------------------------------------------
1 | import { createRequestHandler, redirectTo } from "./util/cloudfront"
2 | import { extractAndParseCookies, generateCookies } from "./util/cookies"
3 |
4 | // eslint-disable-next-line @typescript-eslint/require-await
5 | export const handler = createRequestHandler(async (config, event) => {
6 | const request = event.Records[0].cf.request
7 | const domainName = request.headers["host"][0].value
8 | const { idToken, accessToken, refreshToken } = extractAndParseCookies(
9 | request.headers,
10 | config.clientId,
11 | )
12 |
13 | if (!idToken) {
14 | return redirectTo(`https://${domainName}${config.signOutRedirectTo}`)
15 | }
16 |
17 | const qs = new URLSearchParams({
18 | logout_uri: `https://${domainName}${config.signOutRedirectTo}`,
19 | client_id: config.clientId,
20 | }).toString()
21 |
22 | return redirectTo(`https://${config.cognitoAuthDomain}/logout?${qs}`, {
23 | cookies: generateCookies({
24 | event: "signOut",
25 | tokens: {
26 | idToken: idToken,
27 | accessToken: accessToken ?? "",
28 | refreshToken: refreshToken ?? "",
29 | },
30 | domainName,
31 | ...config,
32 | }),
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/src/handlers/util/logger.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-argument */
2 | /* eslint-disable @typescript-eslint/no-unsafe-return */
3 | /* eslint-disable @typescript-eslint/no-explicit-any */
4 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
5 |
6 | export enum LogLevel {
7 | "none" = 0,
8 | "error" = 10,
9 | "warn" = 20,
10 | "info" = 30,
11 | "debug" = 40,
12 | }
13 |
14 | export class Logger {
15 | constructor(private logLevel: LogLevel) {}
16 |
17 | private jsonify(args: any[]) {
18 | return args.map((arg: any): any => {
19 | if (typeof arg === "object") {
20 | try {
21 | return JSON.stringify(arg)
22 | } catch {
23 | return arg
24 | }
25 | }
26 | return arg
27 | })
28 | }
29 | public info(...args: any): void {
30 | if (this.logLevel >= LogLevel.info) {
31 | console.log(...this.jsonify(args))
32 | }
33 | }
34 | public warn(...args: any): void {
35 | if (this.logLevel >= LogLevel.warn) {
36 | console.warn(...this.jsonify(args))
37 | }
38 | }
39 | public error(...args: any): void {
40 | if (this.logLevel >= LogLevel.error) {
41 | console.error(...this.jsonify(args))
42 | }
43 | }
44 | public debug(...args: any): void {
45 | if (this.logLevel >= LogLevel.debug) {
46 | console.trace(...this.jsonify(args))
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/client-secret.ts:
--------------------------------------------------------------------------------
1 | import * as cognito from "aws-cdk-lib/aws-cognito"
2 | import * as iam from "aws-cdk-lib/aws-iam"
3 | import * as cr from "aws-cdk-lib/custom-resources"
4 | import { Construct } from "constructs"
5 |
6 | export interface RetrieveClientSecretProps {
7 | client: cognito.IUserPoolClient
8 | userPool: cognito.IUserPool
9 | }
10 |
11 | export class RetrieveClientSecret extends Construct {
12 | public clientSecretValue: string
13 |
14 | constructor(scope: Construct, id: string, props: RetrieveClientSecretProps) {
15 | super(scope, id)
16 |
17 | const clientSecret = new cr.AwsCustomResource(this, "Resource", {
18 | onUpdate: {
19 | service: "CognitoIdentityServiceProvider",
20 | action: "describeUserPoolClient",
21 | parameters: {
22 | UserPoolId: props.userPool.userPoolId,
23 | ClientId: props.client.userPoolClientId,
24 | },
25 | physicalResourceId: cr.PhysicalResourceId.of(
26 | `${props.userPool.userPoolId}-${props.client.userPoolClientId}`,
27 | ),
28 | },
29 | policy: cr.AwsCustomResourcePolicy.fromStatements([
30 | new iam.PolicyStatement({
31 | actions: ["cognito-idp:DescribeUserPoolClient"],
32 | resources: [props.userPool.userPoolArn],
33 | }),
34 | ]),
35 | })
36 |
37 | this.clientSecretValue = clientSecret.getResponseField(
38 | "UserPoolClient.ClientSecret",
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-var-requires
2 | const path = require("path")
3 |
4 | module.exports = {
5 | mode: "production",
6 | target: "node",
7 | node: {
8 | __dirname: false,
9 | },
10 | entry: {
11 | "check-auth/index": "./src/handlers/check-auth.ts",
12 | "generate-secret/index": "./src/handlers/generate-secret.ts",
13 | "http-headers/index": "./src/handlers/http-headers.ts",
14 | "parse-auth/index": "./src/handlers/parse-auth.ts",
15 | "refresh-auth/index": "./src/handlers/refresh-auth.ts",
16 | "sign-out/index": "./src/handlers/sign-out.ts",
17 | },
18 | resolve: {
19 | extensions: [".ts", ".js"],
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.ts$/,
25 | use: "ts-loader",
26 | exclude: /node_modules/,
27 | },
28 | {
29 | test: /\.html$/,
30 | loader: "html-loader",
31 | options: {
32 | minimize: true,
33 | },
34 | },
35 | ],
36 | },
37 | externals: [/^aws-sdk/],
38 | output: {
39 | path: path.resolve(__dirname, "dist"),
40 | filename: "[name].js",
41 | libraryTarget: "commonjs",
42 | },
43 | performance: {
44 | hints: "error",
45 | // Max size of deployment bundle in Lambda@Edge Viewer Request
46 | maxAssetSize: 1048576,
47 | // Max size of deployment bundle in Lambda@Edge Viewer Request
48 | maxEntrypointSize: 1048576,
49 | },
50 | }
51 |
--------------------------------------------------------------------------------
/example/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "npx ts-node --prefer-ts-exts bin/example.ts",
3 | "requireApproval": "never",
4 | "watch": {
5 | "include": [
6 | "**"
7 | ],
8 | "exclude": [
9 | "README.md",
10 | "cdk*.json",
11 | "**/*.d.ts",
12 | "**/*.js",
13 | "tsconfig.json",
14 | "package*.json",
15 | "yarn.lock",
16 | "node_modules",
17 | "test"
18 | ]
19 | },
20 | "context": {
21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
23 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26 | "@aws-cdk/core:checkSecretUsage": true,
27 | "@aws-cdk/aws-iam:minimizePolicies": true,
28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
30 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
31 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
32 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
33 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
34 | "@aws-cdk/core:enablePartitionLiterals": true,
35 | "@aws-cdk/core:target-partitions": [
36 | "aws",
37 | "aws-cn"
38 | ],
39 |
40 | "aws-cdk:enableDiffNoFail": "true",
41 | "@aws-cdk/core:stackRelativeExports": "true",
42 | "@aws-cdk/aws-kms:defaultKeyPolicies": true,
43 | "@aws-cdk/core:newStyleStackSynthesis": true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/handlers/util/nonce.ts:
--------------------------------------------------------------------------------
1 | import { createHmac, randomBytes } from "crypto"
2 | import { Config } from "./config"
3 |
4 | export function checkNonceAge(
5 | nonce: string,
6 | maxAge: number,
7 | ): { clientError: string } | undefined {
8 | // Nonce should not be too old.
9 | const timestamp = parseInt(nonce.slice(0, nonce.indexOf("T")))
10 | if (isNaN(timestamp)) {
11 | return {
12 | clientError: "Invalid nonce",
13 | }
14 | }
15 |
16 | if (timestampInSeconds() - timestamp > maxAge) {
17 | return {
18 | clientError: `Nonce is too old (nonce is from ${new Date(
19 | timestamp * 1000,
20 | ).toISOString()})`,
21 | }
22 | }
23 | }
24 |
25 | export function validateNonce(
26 | nonce: string,
27 | providedHmac: string,
28 | config: Config,
29 | ): { clientError: string } | undefined {
30 | const res1 = checkNonceAge(nonce, config.nonceMaxAge)
31 | if (res1) {
32 | return res1
33 | }
34 |
35 | const calculatedHmac = createNonceHmac(nonce, config)
36 | if (calculatedHmac !== providedHmac) {
37 | return {
38 | clientError: `Nonce signature mismatch! Expected ${calculatedHmac} but got ${providedHmac}`,
39 | }
40 | }
41 | }
42 |
43 | export function generateNonce(): string {
44 | const randomString = randomBytes(16).toString("hex")
45 | return `${timestampInSeconds()}T${randomString}`
46 | }
47 |
48 | export function createNonceHmac(nonce: string, config: Config): string {
49 | return createHmac("sha256", config.nonceSigningSecret)
50 | .update(nonce)
51 | .digest("hex")
52 | }
53 |
54 | function timestampInSeconds() {
55 | return (Date.now() / 1000) | 0
56 | }
57 |
--------------------------------------------------------------------------------
/src/client-update.ts:
--------------------------------------------------------------------------------
1 | import * as cognito from "aws-cdk-lib/aws-cognito"
2 | import * as iam from "aws-cdk-lib/aws-iam"
3 | import * as cr from "aws-cdk-lib/custom-resources"
4 | import { Construct } from "constructs"
5 |
6 | interface ClientUpdateProps {
7 | oauthScopes: string[]
8 | client: cognito.IUserPoolClient
9 | userPool: cognito.IUserPool
10 | callbackUrl: string
11 | signOutUrl: string
12 | identityProviders: string[]
13 | }
14 |
15 | export class ClientUpdate extends Construct {
16 | constructor(scope: Construct, id: string, props: ClientUpdateProps) {
17 | super(scope, id)
18 |
19 | new cr.AwsCustomResource(this, "Resource", {
20 | onUpdate: {
21 | service: "CognitoIdentityServiceProvider",
22 | action: "updateUserPoolClient",
23 | parameters: {
24 | AllowedOAuthFlows: ["code"],
25 | AllowedOAuthFlowsUserPoolClient: true,
26 | SupportedIdentityProviders: props.identityProviders,
27 | AllowedOAuthScopes: props.oauthScopes,
28 | ClientId: props.client.userPoolClientId,
29 | CallbackURLs: [props.callbackUrl],
30 | LogoutURLs: [props.signOutUrl],
31 | UserPoolId: props.userPool.userPoolId,
32 | },
33 | physicalResourceId: cr.PhysicalResourceId.of(
34 | `${props.userPool.userPoolId}-${props.client.userPoolClientId}`,
35 | ),
36 | },
37 | policy: cr.AwsCustomResourcePolicy.fromStatements([
38 | new iam.PolicyStatement({
39 | actions: ["cognito-idp:UpdateUserPoolClient"],
40 | resources: [props.userPool.userPoolArn],
41 | }),
42 | ]),
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/handlers/util/config.ts:
--------------------------------------------------------------------------------
1 | import { parse } from "cookie"
2 | import { readFileSync } from "fs"
3 | import * as path from "path"
4 | import { HttpHeaders } from "./cloudfront"
5 | import { CookieSettings } from "./cookies"
6 | import { Logger, LogLevel } from "./logger"
7 |
8 | export interface StoredConfig {
9 | userPoolId: string
10 | clientId: string
11 | oauthScopes: string[]
12 | cognitoAuthDomain: string
13 | callbackPath: string
14 | signOutRedirectTo: string
15 | signOutPath: string
16 | refreshAuthPath: string
17 | cookieSettings: CookieSettings
18 | httpHeaders: HttpHeaders
19 | clientSecret: string
20 | nonceSigningSecret: string
21 | logLevel: keyof typeof LogLevel
22 | requireGroupAnyOf?: string[] | null
23 | }
24 |
25 | export interface Config extends StoredConfig {
26 | tokenIssuer: string
27 | tokenJwksUri: string
28 | logger: Logger
29 | nonceMaxAge: number
30 | }
31 |
32 | export function getConfig(): Config {
33 | const config = JSON.parse(
34 | readFileSync(path.join(__dirname, "/config.json"), "utf-8"),
35 | ) as StoredConfig
36 |
37 | // Derive the issuer and JWKS uri all JWT's will be signed with from
38 | // the User Pool's ID and region.
39 | const userPoolRegion = /^(\S+?)_\S+$/.exec(config.userPoolId)![1]
40 | const tokenIssuer = `https://cognito-idp.${userPoolRegion}.amazonaws.com/${config.userPoolId}`
41 | const tokenJwksUri = `${tokenIssuer}/.well-known/jwks.json`
42 |
43 | return {
44 | nonceMaxAge:
45 | parseInt(parse(config.cookieSettings.nonce.toLowerCase())["max-age"]) ||
46 | 60 * 60 * 24,
47 | ...config,
48 | tokenIssuer,
49 | tokenJwksUri,
50 | logger: new Logger(LogLevel[config.logLevel]),
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/handlers/util/axios.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
3 |
4 | // Workaround for https://github.com/axios/axios/issues/3219
5 | ///
6 |
7 | import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
8 | import { Agent } from "https"
9 | import { Logger } from "./logger"
10 |
11 | const axiosInstance = axios.create({
12 | httpsAgent: new Agent({ keepAlive: true }),
13 | })
14 |
15 | export async function httpPostWithRetry(
16 | url: string,
17 | data: any,
18 | config: AxiosRequestConfig,
19 | logger: Logger,
20 | ): Promise> {
21 | let attempts = 0
22 | while (true) {
23 | ++attempts
24 | try {
25 | return await axiosInstance.post(url, data, config)
26 | } catch (err: any) {
27 | logger.debug(`HTTP POST to ${url} failed (attempt ${attempts}):`)
28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
29 | logger.debug((err.response && err.response.data) || err)
30 | if (attempts >= 5) {
31 | // Try 5 times at most.
32 | logger.error(
33 | `No success after ${attempts} attempts, seizing further attempts`,
34 | )
35 | throw err
36 | }
37 | if (attempts >= 2) {
38 | // After attempting twice immediately, do some exponential backoff with jitter.
39 | logger.debug(
40 | "Doing exponential backoff with jitter, before attempting HTTP POST again ...",
41 | )
42 | await new Promise((resolve) =>
43 | setTimeout(
44 | resolve,
45 | 25 * (Math.pow(2, attempts) + Math.random() * attempts),
46 | ),
47 | )
48 | logger.debug("Done waiting, will try HTTP POST again now")
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/generate-secret.ts:
--------------------------------------------------------------------------------
1 | import * as lambda from "aws-cdk-lib/aws-lambda"
2 | import * as cr from "aws-cdk-lib/custom-resources"
3 | import * as path from "path"
4 | import { Construct } from "constructs"
5 | import { CustomResource, Stack } from "aws-cdk-lib"
6 |
7 | interface GenerateSecretProps {
8 | /**
9 | * Nonce to force secret update.
10 | */
11 | nonce?: string
12 | }
13 |
14 | /**
15 | * Generate a secret to be used in other parts of the deployment.
16 | */
17 | export class GenerateSecret extends Construct {
18 | public readonly value: string
19 |
20 | constructor(scope: Construct, id: string, props?: GenerateSecretProps) {
21 | super(scope, id)
22 |
23 | const resource = new CustomResource(this, "Resource", {
24 | serviceToken: GenerateSecretProvider.getOrCreate(this).serviceToken,
25 | properties: {
26 | Nonce: props?.nonce ?? "",
27 | },
28 | })
29 |
30 | this.value = resource.getAttString("Value")
31 | }
32 | }
33 |
34 | class GenerateSecretProvider extends Construct {
35 | /**
36 | * Returns the singleton provider.
37 | */
38 | public static getOrCreate(scope: Construct) {
39 | const stack = Stack.of(scope)
40 | const id = "henrist.cloudfront-auth.generate-secret.provider"
41 | return (
42 | (stack.node.tryFindChild(id) as GenerateSecretProvider) ||
43 | new GenerateSecretProvider(stack, id)
44 | )
45 | }
46 |
47 | private readonly provider: cr.Provider
48 | public readonly serviceToken: string
49 |
50 | constructor(scope: Construct, id: string) {
51 | super(scope, id)
52 |
53 | this.provider = new cr.Provider(this, "Provider", {
54 | onEventHandler: new lambda.Function(this, "Function", {
55 | code: lambda.Code.fromAsset(
56 | path.join(__dirname, "../dist/generate-secret"),
57 | ),
58 | handler: "index.handler",
59 | runtime: lambda.Runtime.NODEJS_16_X,
60 | }),
61 | })
62 |
63 | this.serviceToken = this.provider.serviceToken
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CloudFront authorization with Cognito for CDK
2 |
3 | Easily add Cognito-based authorization to your CloudFront distribution,
4 | to place static files behind authorization.
5 |
6 | This is based on https://github.com/aws-samples/cloudfront-authorization-at-edge.
7 |
8 | ## Usage
9 |
10 | ```bash
11 | npm install @henrist/cdk-cloudfront-auth
12 | ```
13 |
14 | Deploy the Lambda@Edge functions to us-east-1:
15 |
16 | ```ts
17 | // In a stack deployed to us-east-1.
18 | const authLambdas = new AuthLambdas(this, "AuthLambdas", {
19 | regions: ["eu-west-1"], // Regions to make Lambda version params available.
20 | })
21 | ```
22 |
23 | Deploy the Cognito and CloudFront setup in whatever region
24 | of your choice:
25 |
26 | ```ts
27 | const auth = new CloudFrontAuth(this, "Auth", {
28 | cognitoAuthDomain: `${domain.domainName}.auth.${region}.amazoncognito.com`,
29 | authLambdas, // AuthLambdas from above
30 | userPool, // Cognito User Pool
31 | })
32 | const distribution = new cloudfront.Distribution(this, "Distribution", {
33 | defaultBehavior: auth.createProtectedBehavior(origin),
34 | additionalBehaviors: auth.createAuthPagesBehaviors(origin),
35 | })
36 | auth.updateClient("ClientUpdate", {
37 | signOutUrl: `https://${distribution.distributionDomainName}${auth.signOutRedirectTo}`,
38 | callbackUrl: `https://${distribution.distributionDomainName}${auth.callbackPath}`,
39 | })
40 | ```
41 |
42 | If using `CloudFrontWebDistribution` instead of `Distribution`:
43 |
44 | ```ts
45 | const distribution = new cloudfront.CloudFrontWebDistribution(this, "Distribution", {
46 | originConfigs: [
47 | {
48 | behaviors: [
49 | ...auth.authPages,
50 | {
51 | isDefaultBehavior: true,
52 | lambdaFunctionAssociations: auth.authFilters,
53 | },
54 | ],
55 | },
56 | ],
57 | })
58 | ```
59 |
60 | ## Customizing authorization
61 |
62 | The `CloudFrontAuth` construct accepts a `requireGroupAnyOf` property
63 | that causes access to be restricted to only users in specific groups.
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@henrist/cdk-cloudfront-auth",
3 | "version": "0.0.0-development",
4 | "description": "CDK Constructs for adding authentication for a CloudFront Distribution",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/henrist/cdk-cloudfront-auth"
8 | },
9 | "scripts": {
10 | "build": "rimraf dist && webpack && tsc",
11 | "watch": "tsc -w",
12 | "test": "jest",
13 | "lint": "eslint .",
14 | "lint:fix": "eslint --fix .",
15 | "prepare": "npm run build && husky install",
16 | "semantic-release": "semantic-release"
17 | },
18 | "keywords": [
19 | "cdk",
20 | "cloudfront",
21 | "authentication"
22 | ],
23 | "license": "MIT",
24 | "main": "lib/index.js",
25 | "types": "lib/index.d.ts",
26 | "files": [
27 | "dist/**/*",
28 | "lib/**/*"
29 | ],
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "devDependencies": {
34 | "@aws-cdk/assert": "2.68.0",
35 | "@commitlint/cli": "17.8.1",
36 | "@commitlint/config-conventional": "17.8.1",
37 | "@types/aws-lambda": "8.10.159",
38 | "@types/cookie": "0.6.0",
39 | "@types/jest": "29.5.14",
40 | "@types/jsonwebtoken": "8.5.9",
41 | "@types/node": "18.19.130",
42 | "@typescript-eslint/eslint-plugin": "5.62.0",
43 | "@typescript-eslint/parser": "5.62.0",
44 | "aws-cdk-lib": "2.76.0",
45 | "aws-sdk": "2.1430.0",
46 | "axios": "1.13.2",
47 | "constructs": "10.4.3",
48 | "cookie": "0.7.2",
49 | "eslint": "8.57.1",
50 | "eslint-config-prettier": "8.10.2",
51 | "eslint-plugin-prettier": "4.2.5",
52 | "html-loader": "4.2.0",
53 | "husky": "8.0.3",
54 | "jest": "29.7.0",
55 | "jest-cdk-snapshot": "2.3.6",
56 | "jsonwebtoken": "9.0.0",
57 | "jwks-rsa": "3.2.0",
58 | "prettier": "2.8.8",
59 | "rimraf": "3.0.2",
60 | "semantic-release": "19.0.5",
61 | "ts-jest": "29.4.6",
62 | "ts-loader": "9.5.4",
63 | "ts-node": "10.9.2",
64 | "typescript": "4.9.5",
65 | "webpack": "5.103.0",
66 | "webpack-cli": "4.10.0"
67 | },
68 | "dependencies": {
69 | "@henrist/cdk-cross-region-params": "^2.0.0",
70 | "@henrist/cdk-lambda-config": "^2.1.0"
71 | },
72 | "peerDependencies": {
73 | "aws-cdk-lib": "^2.0.0"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/example/lib/cognito-user.ts:
--------------------------------------------------------------------------------
1 | import * as constructs from "constructs"
2 | import * as cognito from "aws-cdk-lib/aws-cognito"
3 | import * as iam from "aws-cdk-lib/aws-iam"
4 | import * as cr from "aws-cdk-lib/custom-resources"
5 |
6 | interface Props {
7 | userPool: cognito.IUserPool
8 | email: string
9 | password: string
10 | }
11 |
12 | export class CognitoUser extends constructs.Construct {
13 | constructor(scope: constructs.Construct, id: string, props: Props) {
14 | super(scope, id)
15 |
16 | const user = new cr.AwsCustomResource(this, "User", {
17 | onCreate: {
18 | service: "CognitoIdentityServiceProvider",
19 | action: "adminCreateUser",
20 | parameters: {
21 | UserPoolId: props.userPool.userPoolId,
22 | Username: props.email,
23 | },
24 | physicalResourceId: cr.PhysicalResourceId.of(props.email),
25 | },
26 | onDelete: {
27 | service: "CognitoIdentityServiceProvider",
28 | action: "adminDeleteUser",
29 | parameters: {
30 | UserPoolId: props.userPool.userPoolId,
31 | Username: props.email,
32 | },
33 | physicalResourceId: cr.PhysicalResourceId.of(props.email),
34 | },
35 | policy: cr.AwsCustomResourcePolicy.fromStatements([
36 | new iam.PolicyStatement({
37 | actions: [
38 | "cognito-idp:AdminCreateUser",
39 | "cognito-idp:AdminDeleteUser",
40 | ],
41 | resources: [props.userPool.userPoolArn],
42 | }),
43 | ]),
44 | installLatestAwsSdk: false,
45 | })
46 |
47 | const password = new cr.AwsCustomResource(this, "Password", {
48 | onUpdate: {
49 | service: "CognitoIdentityServiceProvider",
50 | action: "adminSetUserPassword",
51 | parameters: {
52 | UserPoolId: props.userPool.userPoolId,
53 | Username: props.email,
54 | Password: props.password,
55 | Permanent: true,
56 | },
57 | physicalResourceId: cr.PhysicalResourceId.of(`password-test`),
58 | },
59 | policy: cr.AwsCustomResourcePolicy.fromStatements([
60 | new iam.PolicyStatement({
61 | actions: ["cognito-idp:AdminSetUserPassword"],
62 | resources: [props.userPool.userPoolArn],
63 | }),
64 | ]),
65 | installLatestAwsSdk: false,
66 | })
67 |
68 | password.node.addDependency(user)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/handlers/check-auth.test.ts:
--------------------------------------------------------------------------------
1 | import { isAuthorized } from "./check-auth"
2 | import { Config } from "./util/config"
3 | import { IdTokenPayload } from "./util/jwt"
4 | import { Logger, LogLevel } from "./util/logger"
5 |
6 | const baseConfig: Config = {
7 | userPoolId: "dummy",
8 | clientId: "dummy",
9 | oauthScopes: ["dummy"],
10 | cognitoAuthDomain: "dummy",
11 | callbackPath: "/callback",
12 | signOutRedirectTo: "/",
13 | signOutPath: "/sign-out",
14 | refreshAuthPath: "/refresh",
15 | cookieSettings: {
16 | idToken: "Path=/; Secure; HttpOnly; SameSite=Lax",
17 | accessToken: "Path=/; Secure; HttpOnly; SameSite=Lax",
18 | refreshToken: "Path=/; Secure; HttpOnly; SameSite=Lax",
19 | nonce: "Path=/; Secure; HttpOnly; SameSite=Lax",
20 | },
21 | httpHeaders: {},
22 | clientSecret: "dummy",
23 | nonceSigningSecret: "dummy",
24 | logLevel: "info",
25 | requireGroupAnyOf: null,
26 | tokenIssuer: "dummy",
27 | tokenJwksUri: "dummy",
28 | logger: new Logger(LogLevel.info),
29 | nonceMaxAge: 3600,
30 | }
31 |
32 | const baseIdToken: IdTokenPayload = {
33 | aud: "2uogllel57lco86t9e64k4tvce",
34 | auth_time: 1594606384,
35 | exp: 1594761087,
36 | iat: 1594757487,
37 | sub: "a2b8b4ae-fc9e-4f51-9d86-124774d5c04a",
38 | token_use: "id",
39 | "cognito:groups": [],
40 | "cognito:username": "Google_1234",
41 | email: "example@example.com",
42 | given_name: "John",
43 | name: "John Doe",
44 | }
45 |
46 | describe("isAuthorized", () => {
47 | describe("having specified list of groups", () => {
48 | const config: Config = {
49 | ...baseConfig,
50 | requireGroupAnyOf: ["group1", "group2"],
51 | }
52 |
53 | it("should not be authorized if missing groups", () => {
54 | const idToken: IdTokenPayload = {
55 | ...baseIdToken,
56 | "cognito:groups": [],
57 | }
58 |
59 | expect(isAuthorized(config, idToken)).toBe(false)
60 | })
61 |
62 | it("should be authorized if in one of the groups", () => {
63 | const idToken: IdTokenPayload = {
64 | ...baseIdToken,
65 | "cognito:groups": ["group1"],
66 | }
67 |
68 | expect(isAuthorized(config, idToken)).toBe(true)
69 | })
70 | })
71 |
72 | describe("not having specified list of groups", () => {
73 | const config: Config = {
74 | ...baseConfig,
75 | requireGroupAnyOf: null,
76 | }
77 |
78 | it("should always be authorized", () => {
79 | const idToken: IdTokenPayload = {
80 | ...baseIdToken,
81 | "cognito:groups": [],
82 | }
83 |
84 | expect(isAuthorized(config, idToken)).toBe(true)
85 | })
86 | })
87 | })
88 |
--------------------------------------------------------------------------------
/example/lib/main-stack.ts:
--------------------------------------------------------------------------------
1 | import { AuthLambdas, CloudFrontAuth } from "@henrist/cdk-cloudfront-auth"
2 | import * as cdk from "aws-cdk-lib"
3 | import * as cloudfront from "aws-cdk-lib/aws-cloudfront"
4 | import * as origins from "aws-cdk-lib/aws-cloudfront-origins"
5 | import * as cognito from "aws-cdk-lib/aws-cognito"
6 | import * as s3 from "aws-cdk-lib/aws-s3"
7 | import * as s3Deployment from "aws-cdk-lib/aws-s3-deployment"
8 | import * as constructs from "constructs"
9 | import { CognitoUser } from "./cognito-user"
10 |
11 | interface Props extends cdk.StackProps {
12 | authLambdas: AuthLambdas
13 | }
14 |
15 | export class MainStack extends cdk.Stack {
16 | constructor(scope: constructs.Construct, id: string, props: Props) {
17 | super(scope, id, props)
18 |
19 | const bucket = new s3.Bucket(this, "Bucket")
20 |
21 | const userPool = new cognito.UserPool(this, "UserPool", {
22 | signInAliases: {
23 | email: true,
24 | },
25 | passwordPolicy: {
26 | minLength: 6,
27 | requireSymbols: false,
28 | requireUppercase: false,
29 | },
30 | signInCaseSensitive: false,
31 | })
32 |
33 | const domainPrefix = `${this.account}-${this.stackName}`
34 |
35 | userPool.addDomain("UserPoolDomain", {
36 | cognitoDomain: {
37 | domainPrefix,
38 | },
39 | })
40 |
41 | const auth = new CloudFrontAuth(this, "Auth", {
42 | cognitoAuthDomain: `${domainPrefix}.auth.${this.region}.amazoncognito.com`,
43 | authLambdas: props.authLambdas,
44 | userPool,
45 | requireGroupAnyOf: ["test"],
46 | })
47 |
48 | const origin = new origins.S3Origin(bucket)
49 |
50 | const distribution = new cloudfront.Distribution(this, "Distribution", {
51 | defaultBehavior: auth.createProtectedBehavior(origin),
52 | additionalBehaviors: auth.createAuthPagesBehaviors(origin),
53 | defaultRootObject: "index.html",
54 | })
55 |
56 | auth.updateClient("ClientUpdate", {
57 | signOutUrl: `https://${distribution.distributionDomainName}${auth.signOutRedirectTo}`,
58 | callbackUrl: `https://${distribution.distributionDomainName}${auth.callbackPath}`,
59 | })
60 |
61 | new s3Deployment.BucketDeployment(this, "BucketDeployment", {
62 | sources: [s3Deployment.Source.asset("./website")],
63 | destinationBucket: bucket,
64 | distribution,
65 | })
66 |
67 | new CognitoUser(this, "User", {
68 | userPool,
69 | email: "example@example.com",
70 | password: "example",
71 | })
72 |
73 | new cdk.CfnOutput(this, "UrlOutput", {
74 | value: `https://${distribution.domainName}`,
75 | })
76 |
77 | new cdk.CfnOutput(this, "BucketNameOutput", {
78 | value: bucket.bucketName,
79 | })
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/handlers/util/jwt.ts:
--------------------------------------------------------------------------------
1 | import { decode, verify } from "jsonwebtoken"
2 | import jwksClient, { RsaSigningKey, SigningKey } from "jwks-rsa"
3 |
4 | export interface IdTokenPayload {
5 | sub: string
6 | "cognito:groups"?: string[]
7 | "cognito:username"?: string
8 | given_name?: string
9 | aud: string
10 | token_use: "id"
11 | auth_time: number
12 | name?: string
13 | exp: number
14 | iat: number
15 | email?: string
16 | }
17 |
18 | // jwks client is cached at this scope so it can be reused
19 | // across Lambda invocations.
20 | let jwksRsa: jwksClient.JwksClient
21 |
22 | function isRsaSigningKey(key: SigningKey): key is RsaSigningKey {
23 | return "rsaPublicKey" in key
24 | }
25 |
26 | /**
27 | * Retrieves the public key that corresponds to the private key with
28 | * which the token was signed.
29 | */
30 | async function getSigningKey(
31 | jwksUri: string,
32 | kid: string,
33 | ): Promise {
34 | if (!jwksRsa) {
35 | jwksRsa = jwksClient({ cache: true, rateLimit: true, jwksUri })
36 | }
37 | const jwk = await jwksRsa.getSigningKey(kid)
38 | return isRsaSigningKey(jwk) ? jwk.rsaPublicKey : jwk.publicKey
39 | }
40 |
41 | export async function validate(
42 | jwtToken: string,
43 | jwksUri: string,
44 | issuer: string,
45 | audience: string,
46 | ): Promise<{ validationError: Error } | undefined> {
47 | const decodedToken = decode(jwtToken, { complete: true })
48 | if (!decodedToken || typeof decodedToken === "string") {
49 | return {
50 | validationError: new Error("Cannot parse JWT token"),
51 | }
52 | }
53 |
54 | // The JWT contains a "kid" claim, key id, that tells which key
55 | // was used to sign the token.
56 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
57 | const kid = decodedToken["header"]["kid"] as string
58 | const jwk = await getSigningKey(jwksUri, kid)
59 | if (jwk instanceof Error) {
60 | return { validationError: jwk }
61 | }
62 |
63 | // Verify the JWT.
64 | // This either rejects (JWT not valid), or resolves (JWT valid).
65 | const verificationOptions = {
66 | audience,
67 | issuer,
68 | ignoreExpiration: false,
69 | }
70 |
71 | return new Promise((resolve) =>
72 | verify(jwtToken, jwk, verificationOptions, (err) =>
73 | err ? resolve({ validationError: err }) : resolve(undefined),
74 | ),
75 | )
76 | }
77 |
78 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
79 | export function decodeIdToken(jwt: string): IdTokenPayload {
80 | const tokenBody = jwt.split(".")[1]
81 | const decodableTokenBody = tokenBody.replace(/-/g, "+").replace(/_/g, "/")
82 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
83 | return JSON.parse(Buffer.from(decodableTokenBody, "base64").toString())
84 | }
85 |
--------------------------------------------------------------------------------
/src/index.test.ts:
--------------------------------------------------------------------------------
1 | import { CloudFrontWebDistribution } from "aws-cdk-lib/aws-cloudfront"
2 | import { UserPool } from "aws-cdk-lib/aws-cognito"
3 | import { CfnVersion } from "aws-cdk-lib/aws-lambda"
4 | import { Bucket } from "aws-cdk-lib/aws-s3"
5 | import "jest-cdk-snapshot"
6 | import { AuthLambdas, CloudFrontAuth } from "."
7 | import { App, Stack } from "aws-cdk-lib"
8 |
9 | test("A simple example", () => {
10 | const app = new App()
11 | const stack1 = new Stack(app, "Stack1", {
12 | env: {
13 | account: "112233445566",
14 | region: "us-east-1",
15 | },
16 | })
17 | const stack2 = new Stack(app, "Stack2", {
18 | env: {
19 | account: "112233445566",
20 | region: "eu-west-1",
21 | },
22 | })
23 |
24 | const authLambdas = new AuthLambdas(stack1, "AuthLambdas", {
25 | regions: ["eu-west-1"],
26 | })
27 |
28 | const userPool = new UserPool(stack2, "UserPool")
29 |
30 | const auth = new CloudFrontAuth(stack2, "Auth", {
31 | cognitoAuthDomain: `my-domain.auth.eu-west-1.amazoncognito.com`,
32 | authLambdas, // AuthLambdas from above
33 | userPool, // Cognito User Pool
34 | })
35 |
36 | const bucket = new Bucket(stack2, "Bucket")
37 |
38 | const distribution = new CloudFrontWebDistribution(
39 | stack2,
40 | "CloudFrontDistribution",
41 | {
42 | originConfigs: [
43 | {
44 | s3OriginSource: {
45 | s3BucketSource: bucket,
46 | },
47 | behaviors: [
48 | ...auth.authPages,
49 | {
50 | isDefaultBehavior: true,
51 | lambdaFunctionAssociations: auth.authFilters,
52 | },
53 | ],
54 | },
55 | ],
56 | },
57 | )
58 |
59 | auth.updateClient("ClientUpdate", {
60 | signOutUrl: `https://${distribution.distributionDomainName}${auth.signOutRedirectTo}`,
61 | callbackUrl: `https://${distribution.distributionDomainName}${auth.callbackPath}`,
62 | })
63 |
64 | expect(stack1).toMatchCdkSnapshot({
65 | ignoreAssets: true,
66 | })
67 | expect(stack2).toMatchCdkSnapshot({
68 | ignoreAssets: true,
69 | })
70 | })
71 |
72 | test("Auth Lambdas with nonce", () => {
73 | const app1 = new App()
74 | const app2 = new App()
75 | const stack1 = new Stack(app1, "Stack", {
76 | env: {
77 | account: "112233445566",
78 | region: "us-east-1",
79 | },
80 | })
81 | const stack2 = new Stack(app2, "Stack", {
82 | env: {
83 | account: "112233445566",
84 | region: "us-east-1",
85 | },
86 | })
87 |
88 | const authLambdas1 = new AuthLambdas(stack1, "AuthLambdas", {
89 | regions: ["eu-west-1"],
90 | })
91 |
92 | const authLambdas2 = new AuthLambdas(stack2, "AuthLambdas", {
93 | regions: ["eu-west-1"],
94 | nonce: "2",
95 | })
96 |
97 | function getLogicalId(scope: AuthLambdas): string {
98 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
99 | return Stack.of(scope).resolve(
100 | (
101 | scope.node
102 | .findChild("ParseAuthFunction")
103 | .node.findChild("CurrentVersion").node.defaultChild as CfnVersion
104 | ).logicalId,
105 | )
106 | }
107 |
108 | const logicalId1 = getLogicalId(authLambdas1)
109 | const logicalId2 = getLogicalId(authLambdas2)
110 |
111 | expect(logicalId1).not.toBe(logicalId2)
112 | })
113 |
--------------------------------------------------------------------------------
/src/handlers/util/cloudfront.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CloudFrontHeaders,
3 | CloudFrontRequestEvent,
4 | CloudFrontRequestHandler,
5 | CloudFrontRequestResult,
6 | CloudFrontResponseEvent,
7 | CloudFrontResponseHandler,
8 | CloudFrontResponseResult,
9 | } from "aws-lambda"
10 | import html from "../error-page/template.html"
11 | import { Config, getConfig } from "./config"
12 |
13 | export type HttpHeaders = Record
14 |
15 | function asCloudFrontHeaders(headers: HttpHeaders): CloudFrontHeaders {
16 | return Object.entries(headers).reduce(
17 | (reduced, [key, value]) =>
18 | Object.assign(reduced, {
19 | [key.toLowerCase()]: [
20 | {
21 | key,
22 | value,
23 | },
24 | ],
25 | }),
26 | {} as CloudFrontHeaders,
27 | )
28 | }
29 |
30 | export function redirectTo(
31 | path: string,
32 | props?: {
33 | cookies?: string[]
34 | },
35 | ): CloudFrontResponseResult {
36 | const headers: CloudFrontHeaders = props?.cookies
37 | ? {
38 | "set-cookie": props.cookies.map((value) => ({
39 | key: "set-cookie",
40 | value,
41 | })),
42 | }
43 | : {}
44 |
45 | return {
46 | status: "307",
47 | statusDescription: "Temporary Redirect",
48 | headers: {
49 | location: [
50 | {
51 | key: "location",
52 | value: path,
53 | },
54 | ],
55 | ...headers,
56 | },
57 | }
58 | }
59 |
60 | export function staticPage(props: {
61 | title: string
62 | message: string
63 | details: string
64 | linkHref: string
65 | linkText: string
66 | statusCode?: string
67 | }): CloudFrontResponseResult {
68 | return {
69 | body: createErrorHtml(props),
70 | status: props.statusCode ?? "500",
71 | headers: {
72 | "content-type": [
73 | {
74 | key: "Content-Type",
75 | value: "text/html; charset=UTF-8",
76 | },
77 | ],
78 | },
79 | }
80 | }
81 |
82 | function createErrorHtml(props: {
83 | title: string
84 | message: string
85 | details: string
86 | linkHref: string
87 | linkText: string
88 | }): string {
89 | const params = { ...props, region: process.env.AWS_REGION }
90 | return html.replace(
91 | /\${([^}]*)}/g,
92 | (_, v: keyof typeof params) => params[v] || "",
93 | )
94 | }
95 |
96 | function addCloudFrontHeaders<
97 | T extends CloudFrontRequestResult | CloudFrontResponseResult,
98 | >(config: Config, response: T): T {
99 | if (!response) {
100 | throw new Error("Expected response value")
101 | }
102 |
103 | return {
104 | ...response,
105 | headers: {
106 | ...(response.headers ?? {}),
107 | ...asCloudFrontHeaders(config.httpHeaders),
108 | },
109 | }
110 | }
111 |
112 | export type RequestHandler = (
113 | config: Config,
114 | event: CloudFrontRequestEvent,
115 | ) => Promise
116 |
117 | export function createRequestHandler(
118 | inner: RequestHandler,
119 | ): CloudFrontRequestHandler {
120 | let config: Config
121 |
122 | return async (event) => {
123 | if (!config) {
124 | config = getConfig()
125 | }
126 |
127 | config.logger.debug("Handling event:", event)
128 |
129 | const response = addCloudFrontHeaders(config, await inner(config, event))
130 |
131 | config.logger.debug("Returning response:", response)
132 | return response
133 | }
134 | }
135 |
136 | export function createResponseHandler(
137 | inner: (
138 | config: Config,
139 | event: CloudFrontResponseEvent,
140 | ) => Promise,
141 | ): CloudFrontResponseHandler {
142 | let config: Config
143 |
144 | return async (event) => {
145 | if (!config) {
146 | config = getConfig()
147 | }
148 |
149 | config.logger.debug("Handling event:", event)
150 |
151 | const response = addCloudFrontHeaders(config, await inner(config, event))
152 |
153 | config.logger.debug("Returning response:", response)
154 | return response
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/handlers/refresh-auth.ts:
--------------------------------------------------------------------------------
1 | import { AxiosResponse } from "axios"
2 | import { httpPostWithRetry } from "./util/axios"
3 | import { createRequestHandler, redirectTo, staticPage } from "./util/cloudfront"
4 | import { extractAndParseCookies, generateCookies } from "./util/cookies"
5 |
6 | export const handler = createRequestHandler(async (config, event) => {
7 | const request = event.Records[0].cf.request
8 | const domainName = request.headers["host"][0].value
9 | let redirectedFromUri = `https://${domainName}`
10 |
11 | function errorResponse(error: string) {
12 | return staticPage({
13 | title: "Refresh issue",
14 | message: "We can't refresh your sign-in because of a technical problem.",
15 | details: error,
16 | linkHref: redirectedFromUri,
17 | linkText: "Try again",
18 | statusCode: "400",
19 | })
20 | }
21 |
22 | const { requestedUri, nonce: currentNonce } = Object.fromEntries(
23 | new URLSearchParams(request.querystring).entries(),
24 | )
25 | redirectedFromUri += requestedUri ?? ""
26 |
27 | const {
28 | idToken,
29 | accessToken,
30 | refreshToken,
31 | nonce: originalNonce,
32 | } = extractAndParseCookies(request.headers, config.clientId)
33 |
34 | if (!idToken || !accessToken || !refreshToken) {
35 | return errorResponse(
36 | "Some of idToken, accessToken and/or refreshToken was not found",
37 | )
38 | }
39 |
40 | try {
41 | validateRefreshRequest(
42 | currentNonce,
43 | originalNonce,
44 | idToken,
45 | accessToken,
46 | refreshToken,
47 | )
48 | } catch (err) {
49 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
50 | return errorResponse(`Failed to refresh tokens: ${err}`)
51 | }
52 |
53 | const headers: Record = {
54 | "Content-Type": "application/x-www-form-urlencoded",
55 | }
56 |
57 | if (config.clientSecret !== "") {
58 | const encodedSecret = Buffer.from(
59 | `${config.clientId}:${config.clientSecret}`,
60 | ).toString("base64")
61 | headers["Authorization"] = `Basic ${encodedSecret}`
62 | }
63 |
64 | let postResult: AxiosResponse<{
65 | id_token: string
66 | access_token: string
67 | }>
68 | try {
69 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
70 | postResult = await httpPostWithRetry(
71 | `https://${config.cognitoAuthDomain}/oauth2/token`,
72 | new URLSearchParams({
73 | grant_type: "refresh_token",
74 | client_id: config.clientId,
75 | refresh_token: refreshToken,
76 | }).toString(),
77 | { headers },
78 | config.logger,
79 | )
80 | } catch (err) {
81 | return redirectTo(redirectedFromUri, {
82 | cookies: generateCookies({
83 | event: "refreshFailed",
84 | tokens: {
85 | idToken: idToken,
86 | accessToken: accessToken,
87 | refreshToken: refreshToken,
88 | },
89 | domainName,
90 | ...config,
91 | }),
92 | })
93 | }
94 |
95 | const updatedTokens = {
96 | idToken: postResult.data.id_token,
97 | accessToken: postResult.data.access_token,
98 | refreshToken: refreshToken,
99 | }
100 |
101 | return redirectTo(redirectedFromUri, {
102 | cookies: generateCookies({
103 | event: "newTokens",
104 | tokens: updatedTokens,
105 | domainName,
106 | ...config,
107 | }),
108 | })
109 | })
110 |
111 | function validateRefreshRequest(
112 | currentNonce?: string | string[],
113 | originalNonce?: string,
114 | idToken?: string,
115 | accessToken?: string,
116 | refreshToken?: string,
117 | ) {
118 | if (!originalNonce) {
119 | throw new Error(
120 | "Your browser didn't send the nonce cookie along, but it is required for security (prevent CSRF).",
121 | )
122 | } else if (currentNonce !== originalNonce) {
123 | throw new Error("Nonce mismatch")
124 | }
125 | Object.entries({ idToken, accessToken, refreshToken }).forEach(
126 | ([tokenType, token]) => {
127 | if (!token) {
128 | throw new Error(`Missing ${tokenType}`)
129 | }
130 | },
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/src/lambdas.ts:
--------------------------------------------------------------------------------
1 | import * as iam from "aws-cdk-lib/aws-iam"
2 | import * as lambda from "aws-cdk-lib/aws-lambda"
3 | import { ParameterResource } from "@henrist/cdk-cross-region-params"
4 | import * as path from "path"
5 | import { Construct } from "constructs"
6 | import { Duration, Stack } from "aws-cdk-lib"
7 |
8 | const isSnapshot = process.env.IS_SNAPSHOT === "true"
9 |
10 | interface AuthLambdasProps {
11 | /**
12 | * List of regions this can be used in. This should contain the region
13 | * where the CloudFront distribution is deployed (the CloudFormation stack).
14 | */
15 | regions: string[]
16 | /**
17 | * A nonce value that can be used to force new lambda functions
18 | * to allow new versions to be created.
19 | */
20 | nonce?: string
21 | }
22 |
23 | /**
24 | * Lambdas used for CloudFront. Must be deployed in us-east-1.
25 | *
26 | * This will provision SSM Parameters the region where the CloudFront
27 | * distribution is deployed from, so that it can be used cross-region.
28 | */
29 | export class AuthLambdas extends Construct {
30 | public readonly checkAuthFn: ParameterResource
31 | public readonly httpHeadersFn: ParameterResource
32 | public readonly parseAuthFn: ParameterResource
33 | public readonly refreshAuthFn: ParameterResource
34 | public readonly signOutFn: ParameterResource
35 |
36 | private readonly regions: string[]
37 | private readonly nonce: string | undefined
38 |
39 | constructor(scope: Construct, id: string, props: AuthLambdasProps) {
40 | super(scope, id)
41 |
42 | const region = Stack.of(this).region
43 | this.regions = props.regions
44 |
45 | this.nonce = props.nonce
46 |
47 | if (region !== "us-east-1") {
48 | throw new Error("Region must be us-east-1 due to Lambda@edge")
49 | }
50 |
51 | const role = new iam.Role(this, "ServiceRole", {
52 | assumedBy: new iam.CompositePrincipal(
53 | new iam.ServicePrincipal("lambda.amazonaws.com"),
54 | new iam.ServicePrincipal("edgelambda.amazonaws.com"),
55 | ),
56 | managedPolicies: [
57 | iam.ManagedPolicy.fromAwsManagedPolicyName(
58 | "service-role/AWSLambdaBasicExecutionRole",
59 | ),
60 | ],
61 | })
62 |
63 | this.checkAuthFn = this.addFunction("CheckAuthFunction", "check-auth", role)
64 | this.httpHeadersFn = this.addFunction(
65 | "HttpHeadersFunction",
66 | "http-headers",
67 | role,
68 | )
69 | this.parseAuthFn = this.addFunction("ParseAuthFunction", "parse-auth", role)
70 | this.refreshAuthFn = this.addFunction(
71 | "RefreshAuthFunction",
72 | "refresh-auth",
73 | role,
74 | )
75 | this.signOutFn = this.addFunction("SignOutFunction", "sign-out", role)
76 | }
77 |
78 | private addFunction(id: string, assetName: string, role: iam.IRole) {
79 | const region = Stack.of(this).region
80 | const stackName = Stack.of(this).stackName
81 |
82 | const fn = new lambda.Function(this, id, {
83 | code:
84 | process.env.NODE_ENV === "test"
85 | ? lambda.Code.fromInline("snapshot-value")
86 | : lambda.Code.fromAsset(path.join(__dirname, `../dist/${assetName}`)),
87 | handler: "index.handler",
88 | runtime: lambda.Runtime.NODEJS_16_X,
89 | timeout: Duration.seconds(5),
90 | role,
91 | description:
92 | this.nonce == null ? undefined : `Nonce value: ${this.nonce}`,
93 | })
94 |
95 | if (this.node.addr === undefined) {
96 | throw new Error("node.addr not found - ensure aws-cdk is up-to-update")
97 | }
98 |
99 | return new ParameterResource(this, `${id}VersionParam`, {
100 | nonce: isSnapshot ? "snapshot" : undefined,
101 | parameterName: `/cf/region/${region}/stack/${stackName}/${this.node.addr}-${id}-function-arn`,
102 | referenceToResource: (scope, id, reference) =>
103 | lambda.Version.fromVersionArn(scope, id, reference),
104 | regions: this.regions,
105 | resource: fn.currentVersion,
106 | resourceToReference: (resource) => resource.functionArn,
107 | })
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/handlers/util/cookies.ts:
--------------------------------------------------------------------------------
1 | import { CloudFrontHeaders } from "aws-lambda"
2 | import { parse } from "cookie"
3 | import { decodeIdToken } from "./jwt"
4 |
5 | type Cookies = Record
6 |
7 | export interface CookieSettings {
8 | idToken: string
9 | accessToken: string
10 | refreshToken: string
11 | nonce: string
12 | }
13 |
14 | /**
15 | * Cookies are present in the HTTP header "Cookie" that may be present
16 | * multiple times. This utility function parses occurrences of that
17 | * header and splits out all the cookies and their values.
18 | * A simple object is returned that allows easy access by cookie
19 | * name: e.g. cookies["nonce"].
20 | */
21 | function extractCookiesFromHeaders(headers: CloudFrontHeaders): Cookies {
22 | if (!headers["cookie"]) {
23 | return {}
24 | }
25 | const cookies = headers["cookie"].reduce(
26 | (reduced, header) => ({
27 | ...reduced,
28 | ...(parse(header.value) as Cookies),
29 | }),
30 | {},
31 | )
32 |
33 | return cookies
34 | }
35 |
36 | function withCookieDomain(
37 | distributionDomainName: string,
38 | cookieSettings: string,
39 | ) {
40 | if (cookieSettings.toLowerCase().indexOf("domain") === -1) {
41 | // Add leading dot for compatibility with Amplify (or js-cookie really).
42 | return `${cookieSettings}; Domain=.${distributionDomainName}`
43 | }
44 | return cookieSettings
45 | }
46 |
47 | export function extractAndParseCookies(
48 | headers: CloudFrontHeaders,
49 | clientId: string,
50 | ): {
51 | tokenUserName?: string
52 | idToken?: string
53 | accessToken?: string
54 | refreshToken?: string
55 | scopes?: string
56 | nonce?: string
57 | nonceHmac?: string
58 | pkce?: string
59 | } {
60 | const cookies = extractCookiesFromHeaders(headers)
61 | if (!cookies) {
62 | return {}
63 | }
64 |
65 | const keyPrefix = `CognitoIdentityServiceProvider.${clientId}`
66 | const tokenUserName = cookies[`${keyPrefix}.LastAuthUser`]
67 |
68 | return {
69 | tokenUserName,
70 | idToken: cookies[`${keyPrefix}.${tokenUserName ?? ""}.idToken`],
71 | accessToken: cookies[`${keyPrefix}.${tokenUserName ?? ""}.accessToken`],
72 | refreshToken: cookies[`${keyPrefix}.${tokenUserName ?? ""}.refreshToken`],
73 | scopes: cookies[`${keyPrefix}.${tokenUserName ?? ""}.tokenScopesString`],
74 | nonce: cookies["spa-auth-edge-nonce"],
75 | nonceHmac: cookies["spa-auth-edge-nonce-hmac"],
76 | pkce: cookies["spa-auth-edge-pkce"],
77 | }
78 | }
79 |
80 | export function generateCookies(param: {
81 | event: "newTokens" | "signOut" | "refreshFailed"
82 | clientId: string
83 | oauthScopes: string[]
84 | domainName: string
85 | cookieSettings: CookieSettings
86 | tokens: {
87 | idToken: string
88 | accessToken: string
89 | refreshToken: string
90 | }
91 | }): string[] {
92 | // Set cookies with the exact names and values Amplify uses
93 | // for seamless interoperability with Amplify.
94 | const decodedIdToken = decodeIdToken(param.tokens.idToken)
95 | const tokenUserName = decodedIdToken["cognito:username"] as string
96 | const keyPrefix = `CognitoIdentityServiceProvider.${param.clientId}`
97 | const idTokenKey = `${keyPrefix}.${tokenUserName}.idToken`
98 | const accessTokenKey = `${keyPrefix}.${tokenUserName}.accessToken`
99 | const refreshTokenKey = `${keyPrefix}.${tokenUserName}.refreshToken`
100 | const lastUserKey = `${keyPrefix}.LastAuthUser`
101 | const scopeKey = `${keyPrefix}.${tokenUserName}.tokenScopesString`
102 | const scopesString = param.oauthScopes.join(" ")
103 | const userDataKey = `${keyPrefix}.${tokenUserName}.userData`
104 | const userData = JSON.stringify({
105 | UserAttributes: [
106 | {
107 | Name: "sub",
108 | Value: decodedIdToken["sub"],
109 | },
110 | {
111 | Name: "email",
112 | Value: decodedIdToken["email"],
113 | },
114 | ],
115 | Username: tokenUserName,
116 | })
117 |
118 | // Construct object with the cookies
119 | const cookies = {
120 | [idTokenKey]: `${param.tokens.idToken}; ${withCookieDomain(
121 | param.domainName,
122 | param.cookieSettings.idToken,
123 | )}`,
124 | [accessTokenKey]: `${param.tokens.accessToken}; ${withCookieDomain(
125 | param.domainName,
126 | param.cookieSettings.accessToken,
127 | )}`,
128 | [refreshTokenKey]: `${param.tokens.refreshToken}; ${withCookieDomain(
129 | param.domainName,
130 | param.cookieSettings.refreshToken,
131 | )}`,
132 | [lastUserKey]: `${tokenUserName}; ${withCookieDomain(
133 | param.domainName,
134 | param.cookieSettings.idToken,
135 | )}`,
136 | [scopeKey]: `${scopesString}; ${withCookieDomain(
137 | param.domainName,
138 | param.cookieSettings.accessToken,
139 | )}`,
140 | [userDataKey]: `${encodeURIComponent(userData)}; ${withCookieDomain(
141 | param.domainName,
142 | param.cookieSettings.idToken,
143 | )}`,
144 | "amplify-signin-with-hostedUI": `true; ${withCookieDomain(
145 | param.domainName,
146 | param.cookieSettings.accessToken,
147 | )}`,
148 | }
149 |
150 | if (param.event === "signOut") {
151 | // Expire all cookies
152 | Object.keys(cookies).forEach(
153 | (key) => (cookies[key] = expireCookie(cookies[key])),
154 | )
155 | } else if (param.event === "refreshFailed") {
156 | // Expire refresh token (so the browser will not send it in vain again)
157 | cookies[refreshTokenKey] = expireCookie(cookies[refreshTokenKey])
158 | }
159 |
160 | // Nonce, nonceHmac and pkce are only used during login phase.
161 | ;[
162 | "spa-auth-edge-nonce",
163 | "spa-auth-edge-nonce-hmac",
164 | "spa-auth-edge-pkce",
165 | ].forEach((key) => {
166 | cookies[key] = expireCookie(cookies[key])
167 | })
168 |
169 | return Object.entries(cookies).map(([k, v]) => `${k}=${v}`)
170 | }
171 |
172 | function expireCookie(cookie = "") {
173 | const cookieParts = cookie
174 | .split(";")
175 | .map((part) => part.trim())
176 | .filter((part) => !part.toLowerCase().startsWith("max-age"))
177 | .filter((part) => !part.toLowerCase().startsWith("expires"))
178 | const expires = `Expires=${new Date(0).toUTCString()}`
179 | // First part is the cookie value, which we'll clear.
180 | return ["", ...cookieParts.slice(1), expires].join("; ")
181 | }
182 |
--------------------------------------------------------------------------------
/src/handlers/check-auth.ts:
--------------------------------------------------------------------------------
1 | import { CloudFrontRequestResult } from "aws-lambda"
2 | import { createHash, randomBytes } from "crypto"
3 | import { safeBase64Stringify } from "./util/base64"
4 | import { createRequestHandler, redirectTo, staticPage } from "./util/cloudfront"
5 | import { Config } from "./util/config"
6 | import { extractAndParseCookies } from "./util/cookies"
7 | import { decodeIdToken, IdTokenPayload, validate } from "./util/jwt"
8 | import { createNonceHmac, generateNonce } from "./util/nonce"
9 |
10 | export const handler = createRequestHandler(async (config, event) => {
11 | const request = event.Records[0].cf.request
12 | const domainName = request.headers["host"][0].value
13 | const requestedUri = `${request.uri}${
14 | request.querystring ? "?" + request.querystring : ""
15 | }`
16 |
17 | const { idToken, refreshToken, nonce, nonceHmac } = extractAndParseCookies(
18 | request.headers,
19 | config.clientId,
20 | )
21 | config.logger.debug("Extracted cookies:", {
22 | idToken,
23 | refreshToken,
24 | nonce,
25 | nonceHmac,
26 | })
27 |
28 | if (!idToken) {
29 | return redirectToSignIn({ config, domainName, requestedUri })
30 | }
31 |
32 | // If the ID token has expired or expires in less than 10 minutes
33 | // and there is a refreshToken: refresh tokens.
34 | // This is done by redirecting the user to the refresh endpoint.
35 | // After the tokens are refreshed the user is redirected back here
36 | // (probably without even noticing this double redirect).
37 | const idTokenPayload = decodeIdToken(idToken)
38 | const { exp } = idTokenPayload
39 | config.logger.debug("ID token exp:", exp, new Date(exp * 1000).toISOString())
40 | if (Date.now() / 1000 > exp - 60 * 10 && refreshToken) {
41 | return redirectToRefresh({ config, domainName, requestedUri })
42 | }
43 |
44 | // Check that the ID token is valid.
45 | config.logger.info("Validating JWT")
46 | const validateResult = await validate(
47 | idToken,
48 | config.tokenJwksUri,
49 | config.tokenIssuer,
50 | config.clientId,
51 | )
52 |
53 | if (validateResult !== undefined) {
54 | config.logger.debug("ID token not valid:", validateResult.validationError)
55 | return redirectToSignIn({ config, domainName, requestedUri })
56 | }
57 |
58 | config.logger.info("JWT is valid")
59 |
60 | if (!isAuthorized(config, idTokenPayload)) {
61 | return staticPage({
62 | title: "Not authorized",
63 | statusCode: "403",
64 | message: "You are not authorized for this resource.",
65 | details:
66 | "Your sign in was successful, but your user is not allowed to access this resource.",
67 | linkHref: `https://${domainName}${config.signOutPath}`,
68 | linkText: "Sign out",
69 | })
70 | }
71 |
72 | return request
73 | })
74 |
75 | /**
76 | * Check if the user is authorized to access the resource.
77 | */
78 | export function isAuthorized(config: Config, idToken: IdTokenPayload): boolean {
79 | if (config.requireGroupAnyOf) {
80 | const inGroups = idToken["cognito:groups"] || []
81 | if (!config.requireGroupAnyOf.some((group) => inGroups.includes(group))) {
82 | return false
83 | }
84 | }
85 |
86 | return true
87 | }
88 |
89 | function redirectToRefresh({
90 | config,
91 | domainName,
92 | requestedUri,
93 | }: {
94 | config: Config
95 | domainName: string
96 | requestedUri: string
97 | }): CloudFrontRequestResult {
98 | config.logger.info("Redirecting to refresh endpoint")
99 | const nonce = generateNonce()
100 | const qs = new URLSearchParams({
101 | requestedUri,
102 | nonce,
103 | }).toString()
104 | return redirectTo(`https://${domainName}${config.refreshAuthPath}?${qs}`, {
105 | cookies: [
106 | `spa-auth-edge-nonce=${encodeURIComponent(nonce)}; ${
107 | config.cookieSettings.nonce
108 | }`,
109 | `spa-auth-edge-nonce-hmac=${encodeURIComponent(
110 | createNonceHmac(nonce, config),
111 | )}; ${config.cookieSettings.nonce}`,
112 | ],
113 | })
114 | }
115 |
116 | function redirectToSignIn({
117 | config,
118 | domainName,
119 | requestedUri,
120 | }: {
121 | config: Config
122 | domainName: string
123 | requestedUri: string
124 | }): CloudFrontRequestResult {
125 | const nonce = generateNonce()
126 | const state = {
127 | nonce,
128 | nonceHmac: createNonceHmac(nonce, config),
129 | ...generatePkceVerifier(config),
130 | }
131 | config.logger.debug("Using new state:", state)
132 |
133 | // Encode the state variable as base64 to avoid a bug in Cognito hosted UI
134 | // when using multiple identity providers.
135 | // Cognito decodes the URL, causing a malformed link due to the JSON string,
136 | // and results in an empty 400 response from Cognito.
137 | const loginQueryString = new URLSearchParams({
138 | redirect_uri: `https://${domainName}${config.callbackPath}`,
139 | response_type: "code",
140 | client_id: config.clientId,
141 | state: safeBase64Stringify(
142 | Buffer.from(
143 | JSON.stringify({ nonce: state.nonce, requestedUri }),
144 | ).toString("base64"),
145 | ),
146 | scope: config.oauthScopes.join(" "),
147 | code_challenge_method: "S256",
148 | code_challenge: state.pkceHash,
149 | }).toString()
150 |
151 | // Return redirect to Cognito Hosted UI for sign-in
152 | return redirectTo(
153 | `https://${config.cognitoAuthDomain}/oauth2/authorize?${loginQueryString}`,
154 | {
155 | cookies: [
156 | `spa-auth-edge-nonce=${encodeURIComponent(state.nonce)}; ${
157 | config.cookieSettings.nonce
158 | }`,
159 | `spa-auth-edge-nonce-hmac=${encodeURIComponent(state.nonceHmac)}; ${
160 | config.cookieSettings.nonce
161 | }`,
162 | `spa-auth-edge-pkce=${encodeURIComponent(state.pkce)}; ${
163 | config.cookieSettings.nonce
164 | }`,
165 | ],
166 | },
167 | )
168 | }
169 |
170 | function generatePkceVerifier(config: Config) {
171 | // Should be between 43 and 128.
172 | // This gives a string on 52 chars.
173 | const pkce = randomBytes(26).toString("hex")
174 |
175 | const verifier = {
176 | pkce,
177 | pkceHash: safeBase64Stringify(
178 | createHash("sha256").update(pkce, "utf8").digest("base64"),
179 | ),
180 | }
181 | config.logger.debug("Generated PKCE verifier:", verifier)
182 | return verifier
183 | }
184 |
--------------------------------------------------------------------------------
/src/handlers/parse-auth.ts:
--------------------------------------------------------------------------------
1 | import { CloudFrontResponseResult } from "aws-lambda"
2 | import { AxiosResponse } from "axios"
3 | import { httpPostWithRetry } from "./util/axios"
4 | import { decodeSafeBase64 } from "./util/base64"
5 | import { createRequestHandler, redirectTo, staticPage } from "./util/cloudfront"
6 | import { Config } from "./util/config"
7 | import { extractAndParseCookies, generateCookies } from "./util/cookies"
8 | import { validate } from "./util/jwt"
9 | import { validateNonce } from "./util/nonce"
10 |
11 | export const handler = createRequestHandler(async (config, event) => {
12 | const request = event.Records[0].cf.request
13 | const domainName = request.headers["host"][0].value
14 |
15 | let redirectedFromUri = `https://${domainName}`
16 | let idToken: string | undefined = undefined
17 |
18 | const cookies = extractAndParseCookies(request.headers, config.clientId)
19 | idToken = cookies.idToken
20 |
21 | const validateResult = validateQueryStringAndCookies({
22 | config,
23 | querystring: request.querystring,
24 | cookies,
25 | })
26 |
27 | if ("clientError" in validateResult) {
28 | return handleFailure({
29 | error: validateResult.clientError,
30 | errorType: "client",
31 | config,
32 | redirectedFromUri,
33 | idToken,
34 | })
35 | } else if ("technicalError" in validateResult) {
36 | return handleFailure({
37 | error: validateResult.technicalError,
38 | errorType: "server",
39 | config,
40 | redirectedFromUri,
41 | idToken,
42 | })
43 | }
44 | const { code, pkce, requestedUri } = validateResult
45 |
46 | config.logger.debug("Query string and cookies are valid")
47 | redirectedFromUri += requestedUri
48 |
49 | const tokens = await exchangeCodeForTokens({
50 | config,
51 | domainName,
52 | code,
53 | pkce,
54 | })
55 | if ("error" in tokens) {
56 | return handleFailure({
57 | error: tokens.error,
58 | errorType: "server",
59 | config,
60 | redirectedFromUri,
61 | idToken,
62 | })
63 | }
64 |
65 | // User is signed in successfully.
66 | return redirectTo(redirectedFromUri, {
67 | cookies: generateCookies({
68 | event: "newTokens",
69 | tokens,
70 | domainName,
71 | ...config,
72 | }),
73 | })
74 | })
75 |
76 | async function handleFailure({
77 | error,
78 | errorType,
79 | config,
80 | idToken,
81 | redirectedFromUri,
82 | }: {
83 | error: string
84 | errorType: "client" | "server"
85 | config: Config
86 | idToken?: string
87 | redirectedFromUri: string
88 | }): Promise {
89 | if (errorType === "client") {
90 | config.logger.warn(error)
91 | } else {
92 | config.logger.error(error)
93 | }
94 |
95 | if (idToken) {
96 | // There is an ID token - maybe the user signed in already (e.g. in another browser tab).
97 | config.logger.debug("ID token found, will check if it is valid")
98 |
99 | config.logger.info("Validating JWT ...")
100 | const validateResult = await validate(
101 | idToken,
102 | config.tokenJwksUri,
103 | config.tokenIssuer,
104 | config.clientId,
105 | )
106 | if (validateResult !== undefined) {
107 | config.logger.debug("ID token not valid:", validateResult.validationError)
108 | }
109 |
110 | config.logger.info("JWT is valid")
111 | // Return user to where he/she came from
112 | return redirectTo(redirectedFromUri)
113 | }
114 |
115 | return staticPage({
116 | title: "Sign-in issue",
117 | message: "We can't sign you in because of a technical problem",
118 | details: errorType === "client" ? error : "Contact administrator",
119 | linkHref: redirectedFromUri,
120 | linkText: "Retry",
121 | statusCode: "503",
122 | })
123 | }
124 |
125 | async function exchangeCodeForTokens({
126 | config,
127 | domainName,
128 | code,
129 | pkce,
130 | }: {
131 | config: Config
132 | domainName: string
133 | code: string
134 | pkce: string
135 | }): Promise<
136 | | {
137 | idToken: string
138 | accessToken: string
139 | refreshToken: string
140 | }
141 | | { error: string }
142 | > {
143 | const cognitoTokenEndpoint = `https://${config.cognitoAuthDomain}/oauth2/token`
144 |
145 | const body = new URLSearchParams({
146 | grant_type: "authorization_code",
147 | client_id: config.clientId,
148 | redirect_uri: `https://${domainName}${config.callbackPath}`,
149 | code,
150 | code_verifier: pkce,
151 | }).toString()
152 |
153 | const requestConfig: Parameters[2] = {
154 | headers: {
155 | "Content-Type": "application/x-www-form-urlencoded",
156 | },
157 | }
158 | if (config.clientSecret) {
159 | const encodedSecret = Buffer.from(
160 | `${config.clientId}:${config.clientSecret}`,
161 | ).toString("base64")
162 | requestConfig.headers!.Authorization = `Basic ${encodedSecret}`
163 | }
164 | config.logger.debug("HTTP POST to Cognito token endpoint:", {
165 | uri: cognitoTokenEndpoint,
166 | body,
167 | requestConfig,
168 | })
169 |
170 | let postResult: AxiosResponse<{
171 | id_token: string
172 | access_token: string
173 | refresh_token: string
174 | }>
175 | try {
176 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
177 | postResult = await httpPostWithRetry(
178 | cognitoTokenEndpoint,
179 | body,
180 | requestConfig,
181 | config.logger,
182 | )
183 | } catch (err) {
184 | return {
185 | // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
186 | error: `Failed to exchange authorization code for tokens: ${err}`,
187 | }
188 | }
189 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
190 | const { status, headers, data: tokens } = postResult
191 |
192 | config.logger.info("Successfully exchanged authorization code for tokens")
193 | config.logger.debug("Response from Cognito token endpoint:", {
194 | status,
195 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
196 | headers,
197 | tokens,
198 | })
199 |
200 | return {
201 | idToken: tokens.id_token,
202 | accessToken: tokens.access_token,
203 | refreshToken: tokens.refresh_token,
204 | }
205 | }
206 |
207 | function validateQueryStringAndCookies(props: {
208 | config: Config
209 | querystring: string
210 | cookies: ReturnType
211 | }):
212 | | { code: string; pkce: string; requestedUri: string }
213 | | { clientError: string }
214 | | { technicalError: string } {
215 | const {
216 | code,
217 | state,
218 | error: cognitoError,
219 | error_description: errorDescription,
220 | } = Object.fromEntries(new URLSearchParams(props.querystring).entries())
221 |
222 | // Check if Cognito threw an Error.
223 | // Cognito puts the error in the query string.
224 | if (cognitoError) {
225 | return {
226 | clientError: `[Cognito] ${cognitoError}: ${errorDescription}`,
227 | }
228 | }
229 |
230 | // The querystring needs to have an authorization code and state.
231 | if (
232 | !code ||
233 | !state ||
234 | typeof code !== "string" ||
235 | typeof state !== "string"
236 | ) {
237 | return {
238 | clientError: [
239 | 'Invalid query string. Your query string does not include parameters "state" and "code".',
240 | "This can happen if your authentication attempt did not originate from this site.",
241 | ].join(" "),
242 | }
243 | }
244 |
245 | // The querystring state should be a JSON string.
246 | let parsedState: { nonce?: string; requestedUri?: string }
247 | try {
248 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
249 | parsedState = JSON.parse(decodeSafeBase64(state))
250 | } catch {
251 | return {
252 | clientError:
253 | 'Invalid query string. Your query string does not include a valid "state" parameter',
254 | }
255 | }
256 |
257 | // The querystring state needs to include the right pieces.
258 | if (!parsedState.requestedUri || !parsedState.nonce) {
259 | return {
260 | clientError:
261 | 'Invalid query string. Your query string does not include a valid "state" parameter',
262 | }
263 | }
264 |
265 | // The querystring state needs to correlate to the cookies.
266 | const { nonce: originalNonce, pkce, nonceHmac } = props.cookies
267 | if (!originalNonce) {
268 | return {
269 | clientError:
270 | "Your browser didn't send the nonce cookie along, but it is required for security (prevent CSRF).",
271 | }
272 | }
273 | if (!pkce) {
274 | return {
275 | clientError:
276 | "Your browser didn't send the pkce cookie along, but it is required for security (prevent CSRF).",
277 | }
278 | }
279 | if (parsedState.nonce !== originalNonce) {
280 | return {
281 | clientError:
282 | "Nonce mismatch. This can happen if you start multiple authentication attempts in parallel (e.g. in separate tabs)",
283 | }
284 | }
285 |
286 | const nonceError = validateNonce(
287 | parsedState.nonce,
288 | nonceHmac ?? "UNKNOWN",
289 | props.config,
290 | )
291 | if (nonceError) {
292 | return nonceError
293 | }
294 |
295 | return { code, pkce, requestedUri: parsedState.requestedUri || "" }
296 | }
297 |
--------------------------------------------------------------------------------
/src/cloudfront-auth.ts:
--------------------------------------------------------------------------------
1 | import * as cloudfront from "aws-cdk-lib/aws-cloudfront"
2 | import {
3 | AddBehaviorOptions,
4 | BehaviorOptions,
5 | IOrigin,
6 | ViewerProtocolPolicy,
7 | } from "aws-cdk-lib/aws-cloudfront"
8 | import * as cognito from "aws-cdk-lib/aws-cognito"
9 | import * as lambda from "aws-cdk-lib/aws-lambda"
10 | import { IVersion } from "aws-cdk-lib/aws-lambda"
11 | import { LambdaConfig } from "@henrist/cdk-lambda-config"
12 | import { RetrieveClientSecret } from "./client-secret"
13 | import { ClientUpdate } from "./client-update"
14 | import { GenerateSecret } from "./generate-secret"
15 | import { StoredConfig } from "./handlers/util/config"
16 | import { AuthLambdas } from "./lambdas"
17 | import { Construct } from "constructs"
18 |
19 | export interface CloudFrontAuthProps {
20 | /**
21 | * Cognito Client that will be used to authenticate the user.
22 | *
23 | * If a custom client is provided, the updateClient method cannot
24 | * be used since we cannot know which parameters was set.
25 | *
26 | * @default - a new client will be generated
27 | */
28 | client?: cognito.UserPoolClient
29 | userPool: cognito.IUserPool
30 | /**
31 | * The domain that is used for Cognito Auth.
32 | *
33 | * If not using custom domains this will be a name under amazoncognito.com.
34 | *
35 | * @example `${domain.domainName}.auth.${region}.amazoncognito.com`
36 | */
37 | cognitoAuthDomain: string
38 | authLambdas: AuthLambdas
39 | /**
40 | * @default /auth/callback
41 | */
42 | callbackPath?: string
43 | /**
44 | * @default /
45 | */
46 | signOutRedirectTo?: string
47 | /**
48 | * @default /auth/sign-out
49 | */
50 | signOutPath?: string
51 | /**
52 | * @default /auth/refresh
53 | */
54 | refreshAuthPath?: string
55 | /**
56 | * Log level.
57 | *
58 | * A log level of debug will log secrets and should only be used in
59 | * a development environment.
60 | *
61 | * @default warn
62 | */
63 | logLevel?: "none" | "error" | "warn" | "info" | "debug"
64 | /**
65 | * Require the user to be part of a specific Cognito group to
66 | * access any resource.
67 | */
68 | requireGroupAnyOf?: string[]
69 | }
70 |
71 | export interface UpdateClientProps {
72 | signOutUrl: string
73 | callbackUrl: string
74 | /**
75 | * List of identity providers used for the client.
76 | *
77 | * @default - COGNITO and identity providers registered in the UserPool construct
78 | */
79 | identityProviders?: string[]
80 | }
81 |
82 | /**
83 | * Configure previously deployed lambda functions, Cognito client
84 | * and CloudFront distribution.
85 | */
86 | export class CloudFrontAuth extends Construct {
87 | public readonly callbackPath: string
88 | public readonly signOutRedirectTo: string
89 | public readonly signOutPath: string
90 | public readonly refreshAuthPath: string
91 |
92 | private readonly userPool: cognito.IUserPool
93 | private readonly clientCreated: boolean
94 | public readonly client: cognito.UserPoolClient
95 |
96 | private readonly checkAuthFn: lambda.IVersion
97 | private readonly httpHeadersFn: lambda.IVersion
98 | private readonly parseAuthFn: lambda.IVersion
99 | private readonly refreshAuthFn: lambda.IVersion
100 | private readonly signOutFn: lambda.IVersion
101 |
102 | private readonly oauthScopes: string[]
103 |
104 | constructor(scope: Construct, id: string, props: CloudFrontAuthProps) {
105 | super(scope, id)
106 |
107 | this.callbackPath = props.callbackPath ?? "/auth/callback"
108 | this.signOutRedirectTo = props.signOutRedirectTo ?? "/"
109 | this.signOutPath = props.signOutPath ?? "/auth/sign-out"
110 | this.refreshAuthPath = props.refreshAuthPath ?? "/auth/refresh"
111 |
112 | this.oauthScopes = [
113 | "phone",
114 | "email",
115 | "profile",
116 | "openid",
117 | "aws.cognito.signin.user.admin",
118 | ]
119 |
120 | this.userPool = props.userPool
121 |
122 | this.clientCreated = !props.client
123 | this.client =
124 | props.client ??
125 | props.userPool.addClient("UserPoolClient", {
126 | // Note: The following must be kept in sync with the API
127 | // call performed in ClientUpdate.
128 | authFlows: {
129 | userPassword: true,
130 | userSrp: true,
131 | },
132 | oAuth: {
133 | flows: {
134 | authorizationCodeGrant: true,
135 | },
136 | },
137 | preventUserExistenceErrors: true,
138 | generateSecret: true,
139 | })
140 |
141 | const nonceSigningSecret = new GenerateSecret(this, "NonceSigningSecret")
142 | .value
143 |
144 | const { clientSecretValue } = new RetrieveClientSecret(
145 | this,
146 | "ClientSecret",
147 | {
148 | client: this.client,
149 | userPool: this.userPool,
150 | },
151 | )
152 |
153 | const config: StoredConfig = {
154 | httpHeaders: {
155 | "Content-Security-Policy":
156 | "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'",
157 | "Strict-Transport-Security":
158 | "max-age=31536000; includeSubdomains; preload",
159 | "Referrer-Policy": "same-origin",
160 | "X-XSS-Protection": "1; mode=block",
161 | "X-Frame-Options": "DENY",
162 | "X-Content-Type-Options": "nosniff",
163 | "Cache-Control": "no-cache",
164 | },
165 | logLevel: props.logLevel ?? "warn",
166 | userPoolId: this.userPool.userPoolId,
167 | clientId: this.client.userPoolClientId,
168 | clientSecret: clientSecretValue,
169 | oauthScopes: this.oauthScopes,
170 | cognitoAuthDomain: props.cognitoAuthDomain,
171 | callbackPath: this.callbackPath,
172 | signOutRedirectTo: this.signOutRedirectTo,
173 | signOutPath: this.signOutPath,
174 | refreshAuthPath: this.refreshAuthPath,
175 | requireGroupAnyOf: props.requireGroupAnyOf,
176 | cookieSettings: {
177 | /*
178 | spaMode - consider if this should be supported
179 | idToken: "Path=/; Secure; SameSite=Lax",
180 | accessToken: "Path=/; Secure; SameSite=Lax",
181 | refreshToken: "Path=/; Secure; SameSite=Lax",
182 | nonce: "Path=/; Secure; HttpOnly; SameSite=Lax",
183 | */
184 | idToken: "Path=/; Secure; HttpOnly; SameSite=Lax",
185 | accessToken: "Path=/; Secure; HttpOnly; SameSite=Lax",
186 | refreshToken: "Path=/; Secure; HttpOnly; SameSite=Lax",
187 | nonce: "Path=/; Secure; HttpOnly; SameSite=Lax",
188 | },
189 | nonceSigningSecret,
190 | }
191 |
192 | this.checkAuthFn = new LambdaConfig(this, "CheckAuthFn", {
193 | function: props.authLambdas.checkAuthFn.get(this, "CheckAuthFnImport"),
194 | config,
195 | }).version
196 |
197 | this.httpHeadersFn = new LambdaConfig(this, "HttpHeadersFn", {
198 | function: props.authLambdas.httpHeadersFn.get(
199 | this,
200 | "HttpHeadersFnImport",
201 | ),
202 | config,
203 | }).version
204 |
205 | this.parseAuthFn = new LambdaConfig(this, "ParseAuthFn", {
206 | function: props.authLambdas.parseAuthFn.get(this, "ParseAuthFnImport"),
207 | config,
208 | }).version
209 |
210 | this.refreshAuthFn = new LambdaConfig(this, "RefreshAuthFn", {
211 | function: props.authLambdas.refreshAuthFn.get(
212 | this,
213 | "RefreshAuthFnImport",
214 | ),
215 | config,
216 | }).version
217 |
218 | this.signOutFn = new LambdaConfig(this, "SignOutFn", {
219 | function: props.authLambdas.signOutFn.get(this, "SignOutFnImport"),
220 | config,
221 | }).version
222 | }
223 |
224 | private createPathLambda(
225 | path: string,
226 | fn: lambda.IVersion,
227 | ): cloudfront.Behavior {
228 | return {
229 | pathPattern: path,
230 | forwardedValues: {
231 | queryString: true,
232 | },
233 | lambdaFunctionAssociations: [
234 | {
235 | eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
236 | lambdaFunction: fn,
237 | },
238 | ],
239 | }
240 | }
241 |
242 | /**
243 | * Create behaviors for authentication pages:
244 | *
245 | * - callback page
246 | * - refresh page
247 | * - sign out page
248 | *
249 | * This is to be used with CloudFrontWebDistribution. See
250 | * createAuthPagesBehaviors if using Distribution.
251 | */
252 | public get authPages(): cloudfront.Behavior[] {
253 | return [
254 | this.createPathLambda(this.callbackPath, this.parseAuthFn),
255 | this.createPathLambda(this.refreshAuthPath, this.refreshAuthFn),
256 | this.createPathLambda(this.signOutPath, this.signOutFn),
257 | ]
258 | }
259 |
260 | /**
261 | * Create behaviors for authentication pages.
262 | *
263 | * - callback page
264 | * - refresh page
265 | * - sign out page
266 | *
267 | * This is to be used with Distribution.
268 | */
269 | public createAuthPagesBehaviors(
270 | origin: IOrigin,
271 | options?: AddBehaviorOptions,
272 | ): Record {
273 | function path(path: string, fn: IVersion): Record {
274 | return {
275 | [path]: {
276 | origin,
277 | compress: true,
278 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
279 | edgeLambdas: [
280 | {
281 | eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
282 | functionVersion: fn,
283 | },
284 | ],
285 | ...options,
286 | },
287 | }
288 | }
289 |
290 | return {
291 | ...path(this.callbackPath, this.parseAuthFn),
292 | ...path(this.refreshAuthPath, this.refreshAuthFn),
293 | ...path(this.signOutPath, this.signOutFn),
294 | }
295 | }
296 |
297 | /**
298 | * Create lambda function association for viewer request to check
299 | * authentication and original response to add headers.
300 | *
301 | * This is to be used with CloudFrontWebDistribution. See
302 | * createProtectedBehavior if using Distribution.
303 | */
304 | public get authFilters(): cloudfront.LambdaFunctionAssociation[] {
305 | return [
306 | {
307 | eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
308 | lambdaFunction: this.checkAuthFn,
309 | },
310 | {
311 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
312 | lambdaFunction: this.httpHeadersFn,
313 | },
314 | ]
315 | }
316 |
317 | /**
318 | * Create behavior that includes authorization check.
319 | *
320 | * This is to be used with Distribution.
321 | */
322 | public createProtectedBehavior(
323 | origin: IOrigin,
324 | options?: AddBehaviorOptions,
325 | ): BehaviorOptions {
326 | if (options?.edgeLambdas != null) {
327 | throw Error("User-defined edgeLambdas is currently not supported")
328 | }
329 |
330 | return {
331 | origin,
332 | compress: true,
333 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
334 | edgeLambdas: [
335 | {
336 | eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST,
337 | functionVersion: this.checkAuthFn,
338 | },
339 | {
340 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE,
341 | functionVersion: this.httpHeadersFn,
342 | },
343 | ],
344 | ...options,
345 | }
346 | }
347 |
348 | /**
349 | * Update Cognito client to use the proper URLs and OAuth scopes.
350 | *
351 | * TODO: In case the client configuration changes and is updated
352 | * by CloudFormation, this will not be reapplied causing the client
353 | * to not be correctly configured.
354 | * How can we avoid this scenario?
355 | */
356 | public updateClient(id: string, props: UpdateClientProps): ClientUpdate {
357 | if (!this.clientCreated) {
358 | throw new Error(
359 | "You cannot use updateClient with a user-provided Cognito Client " +
360 | "since it would override the user-provided settings",
361 | )
362 | }
363 |
364 | return new ClientUpdate(this, id, {
365 | client: this.client,
366 | userPool: this.userPool,
367 | signOutUrl: props.signOutUrl,
368 | callbackUrl: props.callbackUrl,
369 | oauthScopes: this.oauthScopes,
370 | identityProviders:
371 | props.identityProviders ??
372 | ["COGNITO"].concat(
373 | this.userPool.identityProviders.map((it) => it.providerName),
374 | ),
375 | })
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/example/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "0.0.0-development",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "example",
9 | "version": "0.0.0-development",
10 | "dependencies": {
11 | "@henrist/cdk-cloudfront-auth": "^2",
12 | "aws-cdk-lib": "^2",
13 | "source-map-support": "^0.5"
14 | },
15 | "devDependencies": {
16 | "@types/node": "18.19.130",
17 | "aws-cdk": "2.1033.0",
18 | "ts-node": "10.9.2",
19 | "typescript": "4.9.5"
20 | }
21 | },
22 | "node_modules/@aws-cdk/asset-awscli-v1": {
23 | "version": "2.2.200",
24 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.200.tgz",
25 | "integrity": "sha512-Kf5J8DfJK4wZFWT2Myca0lhwke7LwHcHBo+4TvWOGJrFVVKVuuiLCkzPPRBQQVDj0Vtn2NBokZAz8pfMpAqAKg=="
26 | },
27 | "node_modules/@aws-cdk/asset-kubectl-v20": {
28 | "version": "2.1.2",
29 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz",
30 | "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg=="
31 | },
32 | "node_modules/@aws-cdk/asset-node-proxy-agent-v5": {
33 | "version": "2.0.165",
34 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.165.tgz",
35 | "integrity": "sha512-bsyLQD/vqXQcc9RDmlM1XqiFNO/yewgVFXmkMcQkndJbmE/jgYkzewwYGrBlfL725hGLQipXq19+jwWwdsXQqg=="
36 | },
37 | "node_modules/@cspotcode/source-map-support": {
38 | "version": "0.8.1",
39 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
40 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
41 | "dev": true,
42 | "dependencies": {
43 | "@jridgewell/trace-mapping": "0.3.9"
44 | },
45 | "engines": {
46 | "node": ">=12"
47 | }
48 | },
49 | "node_modules/@henrist/cdk-cloudfront-auth": {
50 | "version": "2.1.41",
51 | "resolved": "https://registry.npmjs.org/@henrist/cdk-cloudfront-auth/-/cdk-cloudfront-auth-2.1.41.tgz",
52 | "integrity": "sha512-w7DJco7lZ4TgtprARiinh0Zet1UQbOGvNP5efRz9mYqCGOay539jnFpGLf/GAc2gWcb3+/BrTg1miKJgWh/PyQ==",
53 | "dependencies": {
54 | "@henrist/cdk-cross-region-params": "^2.0.0",
55 | "@henrist/cdk-lambda-config": "^2.1.0"
56 | },
57 | "peerDependencies": {
58 | "aws-cdk-lib": "^2.0.0"
59 | }
60 | },
61 | "node_modules/@henrist/cdk-cross-region-params": {
62 | "version": "2.0.0",
63 | "resolved": "https://registry.npmjs.org/@henrist/cdk-cross-region-params/-/cdk-cross-region-params-2.0.0.tgz",
64 | "integrity": "sha512-r/TWZt0ILAGPjUMu7ZOO4JYLuctUOQeM0hG9QKj/OyE1/hJ2dUhdXhMn2ZI95IcCldGuuyRg0uAEf4VZ36GcGQ==",
65 | "peerDependencies": {
66 | "aws-cdk-lib": "^2.0.0"
67 | }
68 | },
69 | "node_modules/@henrist/cdk-lambda-config": {
70 | "version": "2.1.42",
71 | "resolved": "https://registry.npmjs.org/@henrist/cdk-lambda-config/-/cdk-lambda-config-2.1.42.tgz",
72 | "integrity": "sha512-1jWXTuJr9ShNnxFuW/FqrXdWbG34Q3tic/G5hk3BCCMJHhK0vUCEZOPAfNJ2tHGBeHJ14qOqOAscatNBVQ9l1Q==",
73 | "peerDependencies": {
74 | "aws-cdk-lib": "^2.0.0",
75 | "constructs": "^10.0.0"
76 | }
77 | },
78 | "node_modules/@jridgewell/resolve-uri": {
79 | "version": "3.1.1",
80 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
81 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
82 | "dev": true,
83 | "engines": {
84 | "node": ">=6.0.0"
85 | }
86 | },
87 | "node_modules/@jridgewell/sourcemap-codec": {
88 | "version": "1.4.15",
89 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
90 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
91 | "dev": true
92 | },
93 | "node_modules/@jridgewell/trace-mapping": {
94 | "version": "0.3.9",
95 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
96 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
97 | "dev": true,
98 | "dependencies": {
99 | "@jridgewell/resolve-uri": "^3.0.3",
100 | "@jridgewell/sourcemap-codec": "^1.4.10"
101 | }
102 | },
103 | "node_modules/@tsconfig/node10": {
104 | "version": "1.0.9",
105 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
106 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
107 | "dev": true
108 | },
109 | "node_modules/@tsconfig/node12": {
110 | "version": "1.0.11",
111 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
112 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
113 | "dev": true
114 | },
115 | "node_modules/@tsconfig/node14": {
116 | "version": "1.0.3",
117 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
118 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
119 | "dev": true
120 | },
121 | "node_modules/@tsconfig/node16": {
122 | "version": "1.0.4",
123 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
124 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
125 | "dev": true
126 | },
127 | "node_modules/@types/node": {
128 | "version": "18.19.130",
129 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
130 | "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
131 | "dev": true,
132 | "dependencies": {
133 | "undici-types": "~5.26.4"
134 | }
135 | },
136 | "node_modules/acorn": {
137 | "version": "8.10.0",
138 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
139 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
140 | "dev": true,
141 | "bin": {
142 | "acorn": "bin/acorn"
143 | },
144 | "engines": {
145 | "node": ">=0.4.0"
146 | }
147 | },
148 | "node_modules/acorn-walk": {
149 | "version": "8.2.0",
150 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
151 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
152 | "dev": true,
153 | "engines": {
154 | "node": ">=0.4.0"
155 | }
156 | },
157 | "node_modules/arg": {
158 | "version": "4.1.3",
159 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
160 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
161 | "dev": true
162 | },
163 | "node_modules/aws-cdk": {
164 | "version": "2.1033.0",
165 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1033.0.tgz",
166 | "integrity": "sha512-Pit2k7cVAwxoYI7RMVsOyltuy7/HGENLupJ4KAm/d8mGzOfX+SLOo9YQsx5CKY9J6ErCZ1ViLerklTfjytvQww==",
167 | "dev": true,
168 | "bin": {
169 | "cdk": "bin/cdk"
170 | },
171 | "engines": {
172 | "node": ">= 18.0.0"
173 | },
174 | "optionalDependencies": {
175 | "fsevents": "2.3.2"
176 | }
177 | },
178 | "node_modules/aws-cdk-lib": {
179 | "version": "2.87.0",
180 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.87.0.tgz",
181 | "integrity": "sha512-9kirXX7L7OP/yGmCbaYlkt5OAtowGiGw0AYFIQvSwvx/UU3aJO5XuDwAgDsvToDkRpBi0yX0bNwqa0DItu+C6A==",
182 | "bundleDependencies": [
183 | "@balena/dockerignore",
184 | "case",
185 | "fs-extra",
186 | "ignore",
187 | "jsonschema",
188 | "minimatch",
189 | "punycode",
190 | "semver",
191 | "table",
192 | "yaml"
193 | ],
194 | "dependencies": {
195 | "@aws-cdk/asset-awscli-v1": "^2.2.177",
196 | "@aws-cdk/asset-kubectl-v20": "^2.1.1",
197 | "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148",
198 | "@balena/dockerignore": "^1.0.2",
199 | "case": "1.6.3",
200 | "fs-extra": "^11.1.1",
201 | "ignore": "^5.2.4",
202 | "jsonschema": "^1.4.1",
203 | "minimatch": "^3.1.2",
204 | "punycode": "^2.3.0",
205 | "semver": "^7.5.1",
206 | "table": "^6.8.1",
207 | "yaml": "1.10.2"
208 | },
209 | "engines": {
210 | "node": ">= 14.15.0"
211 | },
212 | "peerDependencies": {
213 | "constructs": "^10.0.0"
214 | }
215 | },
216 | "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
217 | "version": "1.0.2",
218 | "inBundle": true,
219 | "license": "Apache-2.0"
220 | },
221 | "node_modules/aws-cdk-lib/node_modules/ajv": {
222 | "version": "8.12.0",
223 | "inBundle": true,
224 | "license": "MIT",
225 | "dependencies": {
226 | "fast-deep-equal": "^3.1.1",
227 | "json-schema-traverse": "^1.0.0",
228 | "require-from-string": "^2.0.2",
229 | "uri-js": "^4.2.2"
230 | },
231 | "funding": {
232 | "type": "github",
233 | "url": "https://github.com/sponsors/epoberezkin"
234 | }
235 | },
236 | "node_modules/aws-cdk-lib/node_modules/ansi-regex": {
237 | "version": "5.0.1",
238 | "inBundle": true,
239 | "license": "MIT",
240 | "engines": {
241 | "node": ">=8"
242 | }
243 | },
244 | "node_modules/aws-cdk-lib/node_modules/ansi-styles": {
245 | "version": "4.3.0",
246 | "inBundle": true,
247 | "license": "MIT",
248 | "dependencies": {
249 | "color-convert": "^2.0.1"
250 | },
251 | "engines": {
252 | "node": ">=8"
253 | },
254 | "funding": {
255 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
256 | }
257 | },
258 | "node_modules/aws-cdk-lib/node_modules/astral-regex": {
259 | "version": "2.0.0",
260 | "inBundle": true,
261 | "license": "MIT",
262 | "engines": {
263 | "node": ">=8"
264 | }
265 | },
266 | "node_modules/aws-cdk-lib/node_modules/balanced-match": {
267 | "version": "1.0.2",
268 | "inBundle": true,
269 | "license": "MIT"
270 | },
271 | "node_modules/aws-cdk-lib/node_modules/brace-expansion": {
272 | "version": "1.1.11",
273 | "inBundle": true,
274 | "license": "MIT",
275 | "dependencies": {
276 | "balanced-match": "^1.0.0",
277 | "concat-map": "0.0.1"
278 | }
279 | },
280 | "node_modules/aws-cdk-lib/node_modules/case": {
281 | "version": "1.6.3",
282 | "inBundle": true,
283 | "license": "(MIT OR GPL-3.0-or-later)",
284 | "engines": {
285 | "node": ">= 0.8.0"
286 | }
287 | },
288 | "node_modules/aws-cdk-lib/node_modules/color-convert": {
289 | "version": "2.0.1",
290 | "inBundle": true,
291 | "license": "MIT",
292 | "dependencies": {
293 | "color-name": "~1.1.4"
294 | },
295 | "engines": {
296 | "node": ">=7.0.0"
297 | }
298 | },
299 | "node_modules/aws-cdk-lib/node_modules/color-name": {
300 | "version": "1.1.4",
301 | "inBundle": true,
302 | "license": "MIT"
303 | },
304 | "node_modules/aws-cdk-lib/node_modules/concat-map": {
305 | "version": "0.0.1",
306 | "inBundle": true,
307 | "license": "MIT"
308 | },
309 | "node_modules/aws-cdk-lib/node_modules/emoji-regex": {
310 | "version": "8.0.0",
311 | "inBundle": true,
312 | "license": "MIT"
313 | },
314 | "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": {
315 | "version": "3.1.3",
316 | "inBundle": true,
317 | "license": "MIT"
318 | },
319 | "node_modules/aws-cdk-lib/node_modules/fs-extra": {
320 | "version": "11.1.1",
321 | "inBundle": true,
322 | "license": "MIT",
323 | "dependencies": {
324 | "graceful-fs": "^4.2.0",
325 | "jsonfile": "^6.0.1",
326 | "universalify": "^2.0.0"
327 | },
328 | "engines": {
329 | "node": ">=14.14"
330 | }
331 | },
332 | "node_modules/aws-cdk-lib/node_modules/graceful-fs": {
333 | "version": "4.2.11",
334 | "inBundle": true,
335 | "license": "ISC"
336 | },
337 | "node_modules/aws-cdk-lib/node_modules/ignore": {
338 | "version": "5.2.4",
339 | "inBundle": true,
340 | "license": "MIT",
341 | "engines": {
342 | "node": ">= 4"
343 | }
344 | },
345 | "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": {
346 | "version": "3.0.0",
347 | "inBundle": true,
348 | "license": "MIT",
349 | "engines": {
350 | "node": ">=8"
351 | }
352 | },
353 | "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": {
354 | "version": "1.0.0",
355 | "inBundle": true,
356 | "license": "MIT"
357 | },
358 | "node_modules/aws-cdk-lib/node_modules/jsonfile": {
359 | "version": "6.1.0",
360 | "inBundle": true,
361 | "license": "MIT",
362 | "dependencies": {
363 | "universalify": "^2.0.0"
364 | },
365 | "optionalDependencies": {
366 | "graceful-fs": "^4.1.6"
367 | }
368 | },
369 | "node_modules/aws-cdk-lib/node_modules/jsonschema": {
370 | "version": "1.4.1",
371 | "inBundle": true,
372 | "license": "MIT",
373 | "engines": {
374 | "node": "*"
375 | }
376 | },
377 | "node_modules/aws-cdk-lib/node_modules/lodash.truncate": {
378 | "version": "4.4.2",
379 | "inBundle": true,
380 | "license": "MIT"
381 | },
382 | "node_modules/aws-cdk-lib/node_modules/lru-cache": {
383 | "version": "6.0.0",
384 | "inBundle": true,
385 | "license": "ISC",
386 | "dependencies": {
387 | "yallist": "^4.0.0"
388 | },
389 | "engines": {
390 | "node": ">=10"
391 | }
392 | },
393 | "node_modules/aws-cdk-lib/node_modules/minimatch": {
394 | "version": "3.1.2",
395 | "inBundle": true,
396 | "license": "ISC",
397 | "dependencies": {
398 | "brace-expansion": "^1.1.7"
399 | },
400 | "engines": {
401 | "node": "*"
402 | }
403 | },
404 | "node_modules/aws-cdk-lib/node_modules/punycode": {
405 | "version": "2.3.0",
406 | "inBundle": true,
407 | "license": "MIT",
408 | "engines": {
409 | "node": ">=6"
410 | }
411 | },
412 | "node_modules/aws-cdk-lib/node_modules/require-from-string": {
413 | "version": "2.0.2",
414 | "inBundle": true,
415 | "license": "MIT",
416 | "engines": {
417 | "node": ">=0.10.0"
418 | }
419 | },
420 | "node_modules/aws-cdk-lib/node_modules/semver": {
421 | "version": "7.5.2",
422 | "inBundle": true,
423 | "license": "ISC",
424 | "dependencies": {
425 | "lru-cache": "^6.0.0"
426 | },
427 | "bin": {
428 | "semver": "bin/semver.js"
429 | },
430 | "engines": {
431 | "node": ">=10"
432 | }
433 | },
434 | "node_modules/aws-cdk-lib/node_modules/slice-ansi": {
435 | "version": "4.0.0",
436 | "inBundle": true,
437 | "license": "MIT",
438 | "dependencies": {
439 | "ansi-styles": "^4.0.0",
440 | "astral-regex": "^2.0.0",
441 | "is-fullwidth-code-point": "^3.0.0"
442 | },
443 | "engines": {
444 | "node": ">=10"
445 | },
446 | "funding": {
447 | "url": "https://github.com/chalk/slice-ansi?sponsor=1"
448 | }
449 | },
450 | "node_modules/aws-cdk-lib/node_modules/string-width": {
451 | "version": "4.2.3",
452 | "inBundle": true,
453 | "license": "MIT",
454 | "dependencies": {
455 | "emoji-regex": "^8.0.0",
456 | "is-fullwidth-code-point": "^3.0.0",
457 | "strip-ansi": "^6.0.1"
458 | },
459 | "engines": {
460 | "node": ">=8"
461 | }
462 | },
463 | "node_modules/aws-cdk-lib/node_modules/strip-ansi": {
464 | "version": "6.0.1",
465 | "inBundle": true,
466 | "license": "MIT",
467 | "dependencies": {
468 | "ansi-regex": "^5.0.1"
469 | },
470 | "engines": {
471 | "node": ">=8"
472 | }
473 | },
474 | "node_modules/aws-cdk-lib/node_modules/table": {
475 | "version": "6.8.1",
476 | "inBundle": true,
477 | "license": "BSD-3-Clause",
478 | "dependencies": {
479 | "ajv": "^8.0.1",
480 | "lodash.truncate": "^4.4.2",
481 | "slice-ansi": "^4.0.0",
482 | "string-width": "^4.2.3",
483 | "strip-ansi": "^6.0.1"
484 | },
485 | "engines": {
486 | "node": ">=10.0.0"
487 | }
488 | },
489 | "node_modules/aws-cdk-lib/node_modules/universalify": {
490 | "version": "2.0.0",
491 | "inBundle": true,
492 | "license": "MIT",
493 | "engines": {
494 | "node": ">= 10.0.0"
495 | }
496 | },
497 | "node_modules/aws-cdk-lib/node_modules/uri-js": {
498 | "version": "4.4.1",
499 | "inBundle": true,
500 | "license": "BSD-2-Clause",
501 | "dependencies": {
502 | "punycode": "^2.1.0"
503 | }
504 | },
505 | "node_modules/aws-cdk-lib/node_modules/yallist": {
506 | "version": "4.0.0",
507 | "inBundle": true,
508 | "license": "ISC"
509 | },
510 | "node_modules/aws-cdk-lib/node_modules/yaml": {
511 | "version": "1.10.2",
512 | "inBundle": true,
513 | "license": "ISC",
514 | "engines": {
515 | "node": ">= 6"
516 | }
517 | },
518 | "node_modules/buffer-from": {
519 | "version": "1.1.2",
520 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
521 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
522 | },
523 | "node_modules/constructs": {
524 | "version": "10.2.69",
525 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.2.69.tgz",
526 | "integrity": "sha512-0AiM/uQe5Uk6JVe/62oolmSN2MjbFQkOlYrM3fFGZLKuT+g7xlAI10EebFhyCcZwI2JAcWuWCmmCAyCothxjuw==",
527 | "peer": true,
528 | "engines": {
529 | "node": ">= 16.14.0"
530 | }
531 | },
532 | "node_modules/create-require": {
533 | "version": "1.1.1",
534 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
535 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
536 | "dev": true
537 | },
538 | "node_modules/diff": {
539 | "version": "4.0.2",
540 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
541 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
542 | "dev": true,
543 | "engines": {
544 | "node": ">=0.3.1"
545 | }
546 | },
547 | "node_modules/fsevents": {
548 | "version": "2.3.2",
549 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
550 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
551 | "dev": true,
552 | "hasInstallScript": true,
553 | "optional": true,
554 | "os": [
555 | "darwin"
556 | ],
557 | "engines": {
558 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
559 | }
560 | },
561 | "node_modules/make-error": {
562 | "version": "1.3.6",
563 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
564 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
565 | "dev": true
566 | },
567 | "node_modules/source-map": {
568 | "version": "0.6.1",
569 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
570 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
571 | "engines": {
572 | "node": ">=0.10.0"
573 | }
574 | },
575 | "node_modules/source-map-support": {
576 | "version": "0.5.21",
577 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
578 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
579 | "dependencies": {
580 | "buffer-from": "^1.0.0",
581 | "source-map": "^0.6.0"
582 | }
583 | },
584 | "node_modules/ts-node": {
585 | "version": "10.9.2",
586 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
587 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
588 | "dev": true,
589 | "dependencies": {
590 | "@cspotcode/source-map-support": "^0.8.0",
591 | "@tsconfig/node10": "^1.0.7",
592 | "@tsconfig/node12": "^1.0.7",
593 | "@tsconfig/node14": "^1.0.0",
594 | "@tsconfig/node16": "^1.0.2",
595 | "acorn": "^8.4.1",
596 | "acorn-walk": "^8.1.1",
597 | "arg": "^4.1.0",
598 | "create-require": "^1.1.0",
599 | "diff": "^4.0.1",
600 | "make-error": "^1.1.1",
601 | "v8-compile-cache-lib": "^3.0.1",
602 | "yn": "3.1.1"
603 | },
604 | "bin": {
605 | "ts-node": "dist/bin.js",
606 | "ts-node-cwd": "dist/bin-cwd.js",
607 | "ts-node-esm": "dist/bin-esm.js",
608 | "ts-node-script": "dist/bin-script.js",
609 | "ts-node-transpile-only": "dist/bin-transpile.js",
610 | "ts-script": "dist/bin-script-deprecated.js"
611 | },
612 | "peerDependencies": {
613 | "@swc/core": ">=1.2.50",
614 | "@swc/wasm": ">=1.2.50",
615 | "@types/node": "*",
616 | "typescript": ">=2.7"
617 | },
618 | "peerDependenciesMeta": {
619 | "@swc/core": {
620 | "optional": true
621 | },
622 | "@swc/wasm": {
623 | "optional": true
624 | }
625 | }
626 | },
627 | "node_modules/typescript": {
628 | "version": "4.9.5",
629 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
630 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
631 | "dev": true,
632 | "bin": {
633 | "tsc": "bin/tsc",
634 | "tsserver": "bin/tsserver"
635 | },
636 | "engines": {
637 | "node": ">=4.2.0"
638 | }
639 | },
640 | "node_modules/undici-types": {
641 | "version": "5.26.5",
642 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
643 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
644 | "dev": true
645 | },
646 | "node_modules/v8-compile-cache-lib": {
647 | "version": "3.0.1",
648 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
649 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
650 | "dev": true
651 | },
652 | "node_modules/yn": {
653 | "version": "3.1.1",
654 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
655 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
656 | "dev": true,
657 | "engines": {
658 | "node": ">=6"
659 | }
660 | }
661 | },
662 | "dependencies": {
663 | "@aws-cdk/asset-awscli-v1": {
664 | "version": "2.2.200",
665 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.200.tgz",
666 | "integrity": "sha512-Kf5J8DfJK4wZFWT2Myca0lhwke7LwHcHBo+4TvWOGJrFVVKVuuiLCkzPPRBQQVDj0Vtn2NBokZAz8pfMpAqAKg=="
667 | },
668 | "@aws-cdk/asset-kubectl-v20": {
669 | "version": "2.1.2",
670 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz",
671 | "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg=="
672 | },
673 | "@aws-cdk/asset-node-proxy-agent-v5": {
674 | "version": "2.0.165",
675 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.165.tgz",
676 | "integrity": "sha512-bsyLQD/vqXQcc9RDmlM1XqiFNO/yewgVFXmkMcQkndJbmE/jgYkzewwYGrBlfL725hGLQipXq19+jwWwdsXQqg=="
677 | },
678 | "@cspotcode/source-map-support": {
679 | "version": "0.8.1",
680 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
681 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
682 | "dev": true,
683 | "requires": {
684 | "@jridgewell/trace-mapping": "0.3.9"
685 | }
686 | },
687 | "@henrist/cdk-cloudfront-auth": {
688 | "version": "2.1.41",
689 | "resolved": "https://registry.npmjs.org/@henrist/cdk-cloudfront-auth/-/cdk-cloudfront-auth-2.1.41.tgz",
690 | "integrity": "sha512-w7DJco7lZ4TgtprARiinh0Zet1UQbOGvNP5efRz9mYqCGOay539jnFpGLf/GAc2gWcb3+/BrTg1miKJgWh/PyQ==",
691 | "requires": {
692 | "@henrist/cdk-cross-region-params": "^2.0.0",
693 | "@henrist/cdk-lambda-config": "^2.1.0"
694 | }
695 | },
696 | "@henrist/cdk-cross-region-params": {
697 | "version": "2.0.0",
698 | "resolved": "https://registry.npmjs.org/@henrist/cdk-cross-region-params/-/cdk-cross-region-params-2.0.0.tgz",
699 | "integrity": "sha512-r/TWZt0ILAGPjUMu7ZOO4JYLuctUOQeM0hG9QKj/OyE1/hJ2dUhdXhMn2ZI95IcCldGuuyRg0uAEf4VZ36GcGQ==",
700 | "requires": {}
701 | },
702 | "@henrist/cdk-lambda-config": {
703 | "version": "2.1.42",
704 | "resolved": "https://registry.npmjs.org/@henrist/cdk-lambda-config/-/cdk-lambda-config-2.1.42.tgz",
705 | "integrity": "sha512-1jWXTuJr9ShNnxFuW/FqrXdWbG34Q3tic/G5hk3BCCMJHhK0vUCEZOPAfNJ2tHGBeHJ14qOqOAscatNBVQ9l1Q==",
706 | "requires": {}
707 | },
708 | "@jridgewell/resolve-uri": {
709 | "version": "3.1.1",
710 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
711 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
712 | "dev": true
713 | },
714 | "@jridgewell/sourcemap-codec": {
715 | "version": "1.4.15",
716 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
717 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
718 | "dev": true
719 | },
720 | "@jridgewell/trace-mapping": {
721 | "version": "0.3.9",
722 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
723 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
724 | "dev": true,
725 | "requires": {
726 | "@jridgewell/resolve-uri": "^3.0.3",
727 | "@jridgewell/sourcemap-codec": "^1.4.10"
728 | }
729 | },
730 | "@tsconfig/node10": {
731 | "version": "1.0.9",
732 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
733 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
734 | "dev": true
735 | },
736 | "@tsconfig/node12": {
737 | "version": "1.0.11",
738 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
739 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
740 | "dev": true
741 | },
742 | "@tsconfig/node14": {
743 | "version": "1.0.3",
744 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
745 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
746 | "dev": true
747 | },
748 | "@tsconfig/node16": {
749 | "version": "1.0.4",
750 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
751 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
752 | "dev": true
753 | },
754 | "@types/node": {
755 | "version": "18.19.130",
756 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
757 | "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
758 | "dev": true,
759 | "requires": {
760 | "undici-types": "~5.26.4"
761 | }
762 | },
763 | "acorn": {
764 | "version": "8.10.0",
765 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
766 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
767 | "dev": true
768 | },
769 | "acorn-walk": {
770 | "version": "8.2.0",
771 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
772 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
773 | "dev": true
774 | },
775 | "arg": {
776 | "version": "4.1.3",
777 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
778 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
779 | "dev": true
780 | },
781 | "aws-cdk": {
782 | "version": "2.1033.0",
783 | "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1033.0.tgz",
784 | "integrity": "sha512-Pit2k7cVAwxoYI7RMVsOyltuy7/HGENLupJ4KAm/d8mGzOfX+SLOo9YQsx5CKY9J6ErCZ1ViLerklTfjytvQww==",
785 | "dev": true,
786 | "requires": {
787 | "fsevents": "2.3.2"
788 | }
789 | },
790 | "aws-cdk-lib": {
791 | "version": "2.87.0",
792 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.87.0.tgz",
793 | "integrity": "sha512-9kirXX7L7OP/yGmCbaYlkt5OAtowGiGw0AYFIQvSwvx/UU3aJO5XuDwAgDsvToDkRpBi0yX0bNwqa0DItu+C6A==",
794 | "requires": {
795 | "@aws-cdk/asset-awscli-v1": "^2.2.177",
796 | "@aws-cdk/asset-kubectl-v20": "^2.1.1",
797 | "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148",
798 | "@balena/dockerignore": "^1.0.2",
799 | "case": "1.6.3",
800 | "fs-extra": "^11.1.1",
801 | "ignore": "^5.2.4",
802 | "jsonschema": "^1.4.1",
803 | "minimatch": "^3.1.2",
804 | "punycode": "^2.3.0",
805 | "semver": "^7.5.1",
806 | "table": "^6.8.1",
807 | "yaml": "1.10.2"
808 | },
809 | "dependencies": {
810 | "@balena/dockerignore": {
811 | "version": "1.0.2",
812 | "bundled": true
813 | },
814 | "ajv": {
815 | "version": "8.12.0",
816 | "bundled": true,
817 | "requires": {
818 | "fast-deep-equal": "^3.1.1",
819 | "json-schema-traverse": "^1.0.0",
820 | "require-from-string": "^2.0.2",
821 | "uri-js": "^4.2.2"
822 | }
823 | },
824 | "ansi-regex": {
825 | "version": "5.0.1",
826 | "bundled": true
827 | },
828 | "ansi-styles": {
829 | "version": "4.3.0",
830 | "bundled": true,
831 | "requires": {
832 | "color-convert": "^2.0.1"
833 | }
834 | },
835 | "astral-regex": {
836 | "version": "2.0.0",
837 | "bundled": true
838 | },
839 | "balanced-match": {
840 | "version": "1.0.2",
841 | "bundled": true
842 | },
843 | "brace-expansion": {
844 | "version": "1.1.11",
845 | "bundled": true,
846 | "requires": {
847 | "balanced-match": "^1.0.0",
848 | "concat-map": "0.0.1"
849 | }
850 | },
851 | "case": {
852 | "version": "1.6.3",
853 | "bundled": true
854 | },
855 | "color-convert": {
856 | "version": "2.0.1",
857 | "bundled": true,
858 | "requires": {
859 | "color-name": "~1.1.4"
860 | }
861 | },
862 | "color-name": {
863 | "version": "1.1.4",
864 | "bundled": true
865 | },
866 | "concat-map": {
867 | "version": "0.0.1",
868 | "bundled": true
869 | },
870 | "emoji-regex": {
871 | "version": "8.0.0",
872 | "bundled": true
873 | },
874 | "fast-deep-equal": {
875 | "version": "3.1.3",
876 | "bundled": true
877 | },
878 | "fs-extra": {
879 | "version": "11.1.1",
880 | "bundled": true,
881 | "requires": {
882 | "graceful-fs": "^4.2.0",
883 | "jsonfile": "^6.0.1",
884 | "universalify": "^2.0.0"
885 | }
886 | },
887 | "graceful-fs": {
888 | "version": "4.2.11",
889 | "bundled": true
890 | },
891 | "ignore": {
892 | "version": "5.2.4",
893 | "bundled": true
894 | },
895 | "is-fullwidth-code-point": {
896 | "version": "3.0.0",
897 | "bundled": true
898 | },
899 | "json-schema-traverse": {
900 | "version": "1.0.0",
901 | "bundled": true
902 | },
903 | "jsonfile": {
904 | "version": "6.1.0",
905 | "bundled": true,
906 | "requires": {
907 | "graceful-fs": "^4.1.6",
908 | "universalify": "^2.0.0"
909 | }
910 | },
911 | "jsonschema": {
912 | "version": "1.4.1",
913 | "bundled": true
914 | },
915 | "lodash.truncate": {
916 | "version": "4.4.2",
917 | "bundled": true
918 | },
919 | "lru-cache": {
920 | "version": "6.0.0",
921 | "bundled": true,
922 | "requires": {
923 | "yallist": "^4.0.0"
924 | }
925 | },
926 | "minimatch": {
927 | "version": "3.1.2",
928 | "bundled": true,
929 | "requires": {
930 | "brace-expansion": "^1.1.7"
931 | }
932 | },
933 | "punycode": {
934 | "version": "2.3.0",
935 | "bundled": true
936 | },
937 | "require-from-string": {
938 | "version": "2.0.2",
939 | "bundled": true
940 | },
941 | "semver": {
942 | "version": "7.5.2",
943 | "bundled": true,
944 | "requires": {
945 | "lru-cache": "^6.0.0"
946 | }
947 | },
948 | "slice-ansi": {
949 | "version": "4.0.0",
950 | "bundled": true,
951 | "requires": {
952 | "ansi-styles": "^4.0.0",
953 | "astral-regex": "^2.0.0",
954 | "is-fullwidth-code-point": "^3.0.0"
955 | }
956 | },
957 | "string-width": {
958 | "version": "4.2.3",
959 | "bundled": true,
960 | "requires": {
961 | "emoji-regex": "^8.0.0",
962 | "is-fullwidth-code-point": "^3.0.0",
963 | "strip-ansi": "^6.0.1"
964 | }
965 | },
966 | "strip-ansi": {
967 | "version": "6.0.1",
968 | "bundled": true,
969 | "requires": {
970 | "ansi-regex": "^5.0.1"
971 | }
972 | },
973 | "table": {
974 | "version": "6.8.1",
975 | "bundled": true,
976 | "requires": {
977 | "ajv": "^8.0.1",
978 | "lodash.truncate": "^4.4.2",
979 | "slice-ansi": "^4.0.0",
980 | "string-width": "^4.2.3",
981 | "strip-ansi": "^6.0.1"
982 | }
983 | },
984 | "universalify": {
985 | "version": "2.0.0",
986 | "bundled": true
987 | },
988 | "uri-js": {
989 | "version": "4.4.1",
990 | "bundled": true,
991 | "requires": {
992 | "punycode": "^2.1.0"
993 | }
994 | },
995 | "yallist": {
996 | "version": "4.0.0",
997 | "bundled": true
998 | },
999 | "yaml": {
1000 | "version": "1.10.2",
1001 | "bundled": true
1002 | }
1003 | }
1004 | },
1005 | "buffer-from": {
1006 | "version": "1.1.2",
1007 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
1008 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
1009 | },
1010 | "constructs": {
1011 | "version": "10.2.69",
1012 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.2.69.tgz",
1013 | "integrity": "sha512-0AiM/uQe5Uk6JVe/62oolmSN2MjbFQkOlYrM3fFGZLKuT+g7xlAI10EebFhyCcZwI2JAcWuWCmmCAyCothxjuw==",
1014 | "peer": true
1015 | },
1016 | "create-require": {
1017 | "version": "1.1.1",
1018 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
1019 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
1020 | "dev": true
1021 | },
1022 | "diff": {
1023 | "version": "4.0.2",
1024 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
1025 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
1026 | "dev": true
1027 | },
1028 | "fsevents": {
1029 | "version": "2.3.2",
1030 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
1031 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
1032 | "dev": true,
1033 | "optional": true
1034 | },
1035 | "make-error": {
1036 | "version": "1.3.6",
1037 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
1038 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
1039 | "dev": true
1040 | },
1041 | "source-map": {
1042 | "version": "0.6.1",
1043 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1044 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
1045 | },
1046 | "source-map-support": {
1047 | "version": "0.5.21",
1048 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
1049 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
1050 | "requires": {
1051 | "buffer-from": "^1.0.0",
1052 | "source-map": "^0.6.0"
1053 | }
1054 | },
1055 | "ts-node": {
1056 | "version": "10.9.2",
1057 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
1058 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
1059 | "dev": true,
1060 | "requires": {
1061 | "@cspotcode/source-map-support": "^0.8.0",
1062 | "@tsconfig/node10": "^1.0.7",
1063 | "@tsconfig/node12": "^1.0.7",
1064 | "@tsconfig/node14": "^1.0.0",
1065 | "@tsconfig/node16": "^1.0.2",
1066 | "acorn": "^8.4.1",
1067 | "acorn-walk": "^8.1.1",
1068 | "arg": "^4.1.0",
1069 | "create-require": "^1.1.0",
1070 | "diff": "^4.0.1",
1071 | "make-error": "^1.1.1",
1072 | "v8-compile-cache-lib": "^3.0.1",
1073 | "yn": "3.1.1"
1074 | }
1075 | },
1076 | "typescript": {
1077 | "version": "4.9.5",
1078 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
1079 | "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
1080 | "dev": true
1081 | },
1082 | "undici-types": {
1083 | "version": "5.26.5",
1084 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1085 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
1086 | "dev": true
1087 | },
1088 | "v8-compile-cache-lib": {
1089 | "version": "3.0.1",
1090 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
1091 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
1092 | "dev": true
1093 | },
1094 | "yn": {
1095 | "version": "3.1.1",
1096 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
1097 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
1098 | "dev": true
1099 | }
1100 | }
1101 | }
1102 |
--------------------------------------------------------------------------------
/src/__snapshots__/index.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`A simple example 1`] = `
4 | {
5 | "Resources": {
6 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C": {
7 | "DependsOn": [
8 | "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
9 | ],
10 | "Properties": {
11 | "Code": Any