├── .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, 12 | "Handler": "index.handler", 13 | "Role": { 14 | "Fn::GetAtt": [ 15 | "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 16 | "Arn", 17 | ], 18 | }, 19 | "Runtime": "nodejs14.x", 20 | "Timeout": 120, 21 | }, 22 | "Type": "AWS::Lambda::Function", 23 | }, 24 | "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { 25 | "Properties": { 26 | "AssumeRolePolicyDocument": { 27 | "Statement": [ 28 | { 29 | "Action": "sts:AssumeRole", 30 | "Effect": "Allow", 31 | "Principal": { 32 | "Service": "lambda.amazonaws.com", 33 | }, 34 | }, 35 | ], 36 | "Version": "2012-10-17", 37 | }, 38 | "ManagedPolicyArns": [ 39 | { 40 | "Fn::Join": [ 41 | "", 42 | [ 43 | "arn:", 44 | { 45 | "Ref": "AWS::Partition", 46 | }, 47 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 48 | ], 49 | ], 50 | }, 51 | ], 52 | }, 53 | "Type": "AWS::IAM::Role", 54 | }, 55 | "AuthLambdasCheckAuthFunction6B3C9473": { 56 | "DependsOn": [ 57 | "AuthLambdasServiceRoleF10A0667", 58 | ], 59 | "Properties": { 60 | "Code": Any, 61 | "Handler": "index.handler", 62 | "Role": { 63 | "Fn::GetAtt": [ 64 | "AuthLambdasServiceRoleF10A0667", 65 | "Arn", 66 | ], 67 | }, 68 | "Runtime": "nodejs16.x", 69 | "Timeout": 5, 70 | }, 71 | "Type": "AWS::Lambda::Function", 72 | }, 73 | "AuthLambdasCheckAuthFunctionCurrentVersionF10C4D627dbec5a8502ab12d36dafcbdebcc9fd6": { 74 | "Properties": { 75 | "FunctionName": { 76 | "Ref": "AuthLambdasCheckAuthFunction6B3C9473", 77 | }, 78 | }, 79 | "Type": "AWS::Lambda::Version", 80 | }, 81 | "AuthLambdasCheckAuthFunctionVersionParamParameuwest1Resoure8ACD3434": { 82 | "DeletionPolicy": "Delete", 83 | "DependsOn": [ 84 | "AuthLambdasCheckAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicy088F1114", 85 | ], 86 | "Properties": { 87 | "Create": { 88 | "Fn::Join": [ 89 | "", 90 | [ 91 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-CheckAuthFunction-function-arn","Value":"", 92 | { 93 | "Ref": "AuthLambdasCheckAuthFunctionCurrentVersionF10C4D627dbec5a8502ab12d36dafcbdebcc9fd6", 94 | }, 95 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-CheckAuthFunction-function-arn"}}", 96 | ], 97 | ], 98 | }, 99 | "Delete": "{"service":"SSM","action":"deleteParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-CheckAuthFunction-function-arn"},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-CheckAuthFunction-function-arn"}}", 100 | "InstallLatestAwsSdk": false, 101 | "ServiceToken": { 102 | "Fn::GetAtt": [ 103 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 104 | "Arn", 105 | ], 106 | }, 107 | "Update": { 108 | "Fn::Join": [ 109 | "", 110 | [ 111 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-CheckAuthFunction-function-arn","Value":"", 112 | { 113 | "Ref": "AuthLambdasCheckAuthFunctionCurrentVersionF10C4D627dbec5a8502ab12d36dafcbdebcc9fd6", 114 | }, 115 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-CheckAuthFunction-function-arn"}}", 116 | ], 117 | ], 118 | }, 119 | }, 120 | "Type": "Custom::AWS", 121 | "UpdateReplacePolicy": "Delete", 122 | }, 123 | "AuthLambdasCheckAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicy088F1114": { 124 | "Properties": { 125 | "PolicyDocument": { 126 | "Statement": [ 127 | { 128 | "Action": "ssm:PutParameter", 129 | "Effect": "Allow", 130 | "Resource": "*", 131 | }, 132 | { 133 | "Action": "ssm:DeleteParameter", 134 | "Effect": "Allow", 135 | "Resource": "*", 136 | }, 137 | ], 138 | "Version": "2012-10-17", 139 | }, 140 | "PolicyName": "AuthLambdasCheckAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicy088F1114", 141 | "Roles": [ 142 | { 143 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 144 | }, 145 | ], 146 | }, 147 | "Type": "AWS::IAM::Policy", 148 | }, 149 | "AuthLambdasHttpHeadersFunctionC7A2BAB9": { 150 | "DependsOn": [ 151 | "AuthLambdasServiceRoleF10A0667", 152 | ], 153 | "Properties": { 154 | "Code": Any, 155 | "Handler": "index.handler", 156 | "Role": { 157 | "Fn::GetAtt": [ 158 | "AuthLambdasServiceRoleF10A0667", 159 | "Arn", 160 | ], 161 | }, 162 | "Runtime": "nodejs16.x", 163 | "Timeout": 5, 164 | }, 165 | "Type": "AWS::Lambda::Function", 166 | }, 167 | "AuthLambdasHttpHeadersFunctionCurrentVersion8B8CDE2E7dbec5a8502ab12d36dafcbdebcc9fd6": { 168 | "Properties": { 169 | "FunctionName": { 170 | "Ref": "AuthLambdasHttpHeadersFunctionC7A2BAB9", 171 | }, 172 | }, 173 | "Type": "AWS::Lambda::Version", 174 | }, 175 | "AuthLambdasHttpHeadersFunctionVersionParamParameuwest1Resoure0C3C81F2": { 176 | "DeletionPolicy": "Delete", 177 | "DependsOn": [ 178 | "AuthLambdasHttpHeadersFunctionVersionParamParameuwest1ResoureCustomResourcePolicyC99B8AA6", 179 | ], 180 | "Properties": { 181 | "Create": { 182 | "Fn::Join": [ 183 | "", 184 | [ 185 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-HttpHeadersFunction-function-arn","Value":"", 186 | { 187 | "Ref": "AuthLambdasHttpHeadersFunctionCurrentVersion8B8CDE2E7dbec5a8502ab12d36dafcbdebcc9fd6", 188 | }, 189 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-HttpHeadersFunction-function-arn"}}", 190 | ], 191 | ], 192 | }, 193 | "Delete": "{"service":"SSM","action":"deleteParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-HttpHeadersFunction-function-arn"},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-HttpHeadersFunction-function-arn"}}", 194 | "InstallLatestAwsSdk": false, 195 | "ServiceToken": { 196 | "Fn::GetAtt": [ 197 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 198 | "Arn", 199 | ], 200 | }, 201 | "Update": { 202 | "Fn::Join": [ 203 | "", 204 | [ 205 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-HttpHeadersFunction-function-arn","Value":"", 206 | { 207 | "Ref": "AuthLambdasHttpHeadersFunctionCurrentVersion8B8CDE2E7dbec5a8502ab12d36dafcbdebcc9fd6", 208 | }, 209 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-HttpHeadersFunction-function-arn"}}", 210 | ], 211 | ], 212 | }, 213 | }, 214 | "Type": "Custom::AWS", 215 | "UpdateReplacePolicy": "Delete", 216 | }, 217 | "AuthLambdasHttpHeadersFunctionVersionParamParameuwest1ResoureCustomResourcePolicyC99B8AA6": { 218 | "Properties": { 219 | "PolicyDocument": { 220 | "Statement": [ 221 | { 222 | "Action": "ssm:PutParameter", 223 | "Effect": "Allow", 224 | "Resource": "*", 225 | }, 226 | { 227 | "Action": "ssm:DeleteParameter", 228 | "Effect": "Allow", 229 | "Resource": "*", 230 | }, 231 | ], 232 | "Version": "2012-10-17", 233 | }, 234 | "PolicyName": "AuthLambdasHttpHeadersFunctionVersionParamParameuwest1ResoureCustomResourcePolicyC99B8AA6", 235 | "Roles": [ 236 | { 237 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 238 | }, 239 | ], 240 | }, 241 | "Type": "AWS::IAM::Policy", 242 | }, 243 | "AuthLambdasParseAuthFunctionC04A121B": { 244 | "DependsOn": [ 245 | "AuthLambdasServiceRoleF10A0667", 246 | ], 247 | "Properties": { 248 | "Code": Any, 249 | "Handler": "index.handler", 250 | "Role": { 251 | "Fn::GetAtt": [ 252 | "AuthLambdasServiceRoleF10A0667", 253 | "Arn", 254 | ], 255 | }, 256 | "Runtime": "nodejs16.x", 257 | "Timeout": 5, 258 | }, 259 | "Type": "AWS::Lambda::Function", 260 | }, 261 | "AuthLambdasParseAuthFunctionCurrentVersionA32A59917dbec5a8502ab12d36dafcbdebcc9fd6": { 262 | "Properties": { 263 | "FunctionName": { 264 | "Ref": "AuthLambdasParseAuthFunctionC04A121B", 265 | }, 266 | }, 267 | "Type": "AWS::Lambda::Version", 268 | }, 269 | "AuthLambdasParseAuthFunctionVersionParamParameuwest1Resoure934661C6": { 270 | "DeletionPolicy": "Delete", 271 | "DependsOn": [ 272 | "AuthLambdasParseAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicyCEAE27DB", 273 | ], 274 | "Properties": { 275 | "Create": { 276 | "Fn::Join": [ 277 | "", 278 | [ 279 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-ParseAuthFunction-function-arn","Value":"", 280 | { 281 | "Ref": "AuthLambdasParseAuthFunctionCurrentVersionA32A59917dbec5a8502ab12d36dafcbdebcc9fd6", 282 | }, 283 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-ParseAuthFunction-function-arn"}}", 284 | ], 285 | ], 286 | }, 287 | "Delete": "{"service":"SSM","action":"deleteParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-ParseAuthFunction-function-arn"},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-ParseAuthFunction-function-arn"}}", 288 | "InstallLatestAwsSdk": false, 289 | "ServiceToken": { 290 | "Fn::GetAtt": [ 291 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 292 | "Arn", 293 | ], 294 | }, 295 | "Update": { 296 | "Fn::Join": [ 297 | "", 298 | [ 299 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-ParseAuthFunction-function-arn","Value":"", 300 | { 301 | "Ref": "AuthLambdasParseAuthFunctionCurrentVersionA32A59917dbec5a8502ab12d36dafcbdebcc9fd6", 302 | }, 303 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-ParseAuthFunction-function-arn"}}", 304 | ], 305 | ], 306 | }, 307 | }, 308 | "Type": "Custom::AWS", 309 | "UpdateReplacePolicy": "Delete", 310 | }, 311 | "AuthLambdasParseAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicyCEAE27DB": { 312 | "Properties": { 313 | "PolicyDocument": { 314 | "Statement": [ 315 | { 316 | "Action": "ssm:PutParameter", 317 | "Effect": "Allow", 318 | "Resource": "*", 319 | }, 320 | { 321 | "Action": "ssm:DeleteParameter", 322 | "Effect": "Allow", 323 | "Resource": "*", 324 | }, 325 | ], 326 | "Version": "2012-10-17", 327 | }, 328 | "PolicyName": "AuthLambdasParseAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicyCEAE27DB", 329 | "Roles": [ 330 | { 331 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 332 | }, 333 | ], 334 | }, 335 | "Type": "AWS::IAM::Policy", 336 | }, 337 | "AuthLambdasRefreshAuthFunction4B0B3BD6": { 338 | "DependsOn": [ 339 | "AuthLambdasServiceRoleF10A0667", 340 | ], 341 | "Properties": { 342 | "Code": Any, 343 | "Handler": "index.handler", 344 | "Role": { 345 | "Fn::GetAtt": [ 346 | "AuthLambdasServiceRoleF10A0667", 347 | "Arn", 348 | ], 349 | }, 350 | "Runtime": "nodejs16.x", 351 | "Timeout": 5, 352 | }, 353 | "Type": "AWS::Lambda::Function", 354 | }, 355 | "AuthLambdasRefreshAuthFunctionCurrentVersion632285F67dbec5a8502ab12d36dafcbdebcc9fd6": { 356 | "Properties": { 357 | "FunctionName": { 358 | "Ref": "AuthLambdasRefreshAuthFunction4B0B3BD6", 359 | }, 360 | }, 361 | "Type": "AWS::Lambda::Version", 362 | }, 363 | "AuthLambdasRefreshAuthFunctionVersionParamParameuwest1Resoure0C0D8913": { 364 | "DeletionPolicy": "Delete", 365 | "DependsOn": [ 366 | "AuthLambdasRefreshAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicy095CEC70", 367 | ], 368 | "Properties": { 369 | "Create": { 370 | "Fn::Join": [ 371 | "", 372 | [ 373 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-RefreshAuthFunction-function-arn","Value":"", 374 | { 375 | "Ref": "AuthLambdasRefreshAuthFunctionCurrentVersion632285F67dbec5a8502ab12d36dafcbdebcc9fd6", 376 | }, 377 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-RefreshAuthFunction-function-arn"}}", 378 | ], 379 | ], 380 | }, 381 | "Delete": "{"service":"SSM","action":"deleteParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-RefreshAuthFunction-function-arn"},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-RefreshAuthFunction-function-arn"}}", 382 | "InstallLatestAwsSdk": false, 383 | "ServiceToken": { 384 | "Fn::GetAtt": [ 385 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 386 | "Arn", 387 | ], 388 | }, 389 | "Update": { 390 | "Fn::Join": [ 391 | "", 392 | [ 393 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-RefreshAuthFunction-function-arn","Value":"", 394 | { 395 | "Ref": "AuthLambdasRefreshAuthFunctionCurrentVersion632285F67dbec5a8502ab12d36dafcbdebcc9fd6", 396 | }, 397 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-RefreshAuthFunction-function-arn"}}", 398 | ], 399 | ], 400 | }, 401 | }, 402 | "Type": "Custom::AWS", 403 | "UpdateReplacePolicy": "Delete", 404 | }, 405 | "AuthLambdasRefreshAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicy095CEC70": { 406 | "Properties": { 407 | "PolicyDocument": { 408 | "Statement": [ 409 | { 410 | "Action": "ssm:PutParameter", 411 | "Effect": "Allow", 412 | "Resource": "*", 413 | }, 414 | { 415 | "Action": "ssm:DeleteParameter", 416 | "Effect": "Allow", 417 | "Resource": "*", 418 | }, 419 | ], 420 | "Version": "2012-10-17", 421 | }, 422 | "PolicyName": "AuthLambdasRefreshAuthFunctionVersionParamParameuwest1ResoureCustomResourcePolicy095CEC70", 423 | "Roles": [ 424 | { 425 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 426 | }, 427 | ], 428 | }, 429 | "Type": "AWS::IAM::Policy", 430 | }, 431 | "AuthLambdasServiceRoleF10A0667": { 432 | "Properties": { 433 | "AssumeRolePolicyDocument": { 434 | "Statement": [ 435 | { 436 | "Action": "sts:AssumeRole", 437 | "Effect": "Allow", 438 | "Principal": { 439 | "Service": "lambda.amazonaws.com", 440 | }, 441 | }, 442 | { 443 | "Action": "sts:AssumeRole", 444 | "Effect": "Allow", 445 | "Principal": { 446 | "Service": "edgelambda.amazonaws.com", 447 | }, 448 | }, 449 | ], 450 | "Version": "2012-10-17", 451 | }, 452 | "ManagedPolicyArns": [ 453 | { 454 | "Fn::Join": [ 455 | "", 456 | [ 457 | "arn:", 458 | { 459 | "Ref": "AWS::Partition", 460 | }, 461 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 462 | ], 463 | ], 464 | }, 465 | ], 466 | }, 467 | "Type": "AWS::IAM::Role", 468 | }, 469 | "AuthLambdasSignOutFunction251863E4": { 470 | "DependsOn": [ 471 | "AuthLambdasServiceRoleF10A0667", 472 | ], 473 | "Properties": { 474 | "Code": Any, 475 | "Handler": "index.handler", 476 | "Role": { 477 | "Fn::GetAtt": [ 478 | "AuthLambdasServiceRoleF10A0667", 479 | "Arn", 480 | ], 481 | }, 482 | "Runtime": "nodejs16.x", 483 | "Timeout": 5, 484 | }, 485 | "Type": "AWS::Lambda::Function", 486 | }, 487 | "AuthLambdasSignOutFunctionCurrentVersion0576BBB17dbec5a8502ab12d36dafcbdebcc9fd6": { 488 | "Properties": { 489 | "FunctionName": { 490 | "Ref": "AuthLambdasSignOutFunction251863E4", 491 | }, 492 | }, 493 | "Type": "AWS::Lambda::Version", 494 | }, 495 | "AuthLambdasSignOutFunctionVersionParamParameuwest1Resoure18583D2D": { 496 | "DeletionPolicy": "Delete", 497 | "DependsOn": [ 498 | "AuthLambdasSignOutFunctionVersionParamParameuwest1ResoureCustomResourcePolicy16B30BE4", 499 | ], 500 | "Properties": { 501 | "Create": { 502 | "Fn::Join": [ 503 | "", 504 | [ 505 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-SignOutFunction-function-arn","Value":"", 506 | { 507 | "Ref": "AuthLambdasSignOutFunctionCurrentVersion0576BBB17dbec5a8502ab12d36dafcbdebcc9fd6", 508 | }, 509 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-SignOutFunction-function-arn"}}", 510 | ], 511 | ], 512 | }, 513 | "Delete": "{"service":"SSM","action":"deleteParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-SignOutFunction-function-arn"},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-SignOutFunction-function-arn"}}", 514 | "InstallLatestAwsSdk": false, 515 | "ServiceToken": { 516 | "Fn::GetAtt": [ 517 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 518 | "Arn", 519 | ], 520 | }, 521 | "Update": { 522 | "Fn::Join": [ 523 | "", 524 | [ 525 | "{"service":"SSM","action":"putParameter","parameters":{"Name":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-SignOutFunction-function-arn","Value":"", 526 | { 527 | "Ref": "AuthLambdasSignOutFunctionCurrentVersion0576BBB17dbec5a8502ab12d36dafcbdebcc9fd6", 528 | }, 529 | "","Type":"String","Overwrite":true},"region":"eu-west-1","physicalResourceId":{"id":"/cf/region/us-east-1/stack/Stack1/c822e4a2b5e248e64ee706fb7d4af8c80d9b03944e-SignOutFunction-function-arn"}}", 530 | ], 531 | ], 532 | }, 533 | }, 534 | "Type": "Custom::AWS", 535 | "UpdateReplacePolicy": "Delete", 536 | }, 537 | "AuthLambdasSignOutFunctionVersionParamParameuwest1ResoureCustomResourcePolicy16B30BE4": { 538 | "Properties": { 539 | "PolicyDocument": { 540 | "Statement": [ 541 | { 542 | "Action": "ssm:PutParameter", 543 | "Effect": "Allow", 544 | "Resource": "*", 545 | }, 546 | { 547 | "Action": "ssm:DeleteParameter", 548 | "Effect": "Allow", 549 | "Resource": "*", 550 | }, 551 | ], 552 | "Version": "2012-10-17", 553 | }, 554 | "PolicyName": "AuthLambdasSignOutFunctionVersionParamParameuwest1ResoureCustomResourcePolicy16B30BE4", 555 | "Roles": [ 556 | { 557 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 558 | }, 559 | ], 560 | }, 561 | "Type": "AWS::IAM::Policy", 562 | }, 563 | }, 564 | } 565 | `; 566 | 567 | exports[`A simple example 2`] = ` 568 | { 569 | "Parameters": Any, 570 | "Resources": { 571 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { 572 | "DependsOn": [ 573 | "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 574 | ], 575 | "Properties": { 576 | "Code": Any, 577 | "Handler": "index.handler", 578 | "Role": { 579 | "Fn::GetAtt": [ 580 | "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 581 | "Arn", 582 | ], 583 | }, 584 | "Runtime": "nodejs14.x", 585 | "Timeout": 120, 586 | }, 587 | "Type": "AWS::Lambda::Function", 588 | }, 589 | "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { 590 | "Properties": { 591 | "AssumeRolePolicyDocument": { 592 | "Statement": [ 593 | { 594 | "Action": "sts:AssumeRole", 595 | "Effect": "Allow", 596 | "Principal": { 597 | "Service": "lambda.amazonaws.com", 598 | }, 599 | }, 600 | ], 601 | "Version": "2012-10-17", 602 | }, 603 | "ManagedPolicyArns": [ 604 | { 605 | "Fn::Join": [ 606 | "", 607 | [ 608 | "arn:", 609 | { 610 | "Ref": "AWS::Partition", 611 | }, 612 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 613 | ], 614 | ], 615 | }, 616 | ], 617 | }, 618 | "Type": "AWS::IAM::Role", 619 | }, 620 | "AuthCheckAuthFnC12AAE69": { 621 | "DeletionPolicy": "Delete", 622 | "Properties": { 623 | "Config": { 624 | "callbackPath": "/auth/callback", 625 | "clientId": { 626 | "Ref": "UserPoolUserPoolClient40176907", 627 | }, 628 | "clientSecret": { 629 | "Fn::GetAtt": [ 630 | "AuthClientSecret768E9C19", 631 | "UserPoolClient.ClientSecret", 632 | ], 633 | }, 634 | "cognitoAuthDomain": "my-domain.auth.eu-west-1.amazoncognito.com", 635 | "cookieSettings": { 636 | "accessToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 637 | "idToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 638 | "nonce": "Path=/; Secure; HttpOnly; SameSite=Lax", 639 | "refreshToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 640 | }, 641 | "httpHeaders": { 642 | "Cache-Control": "no-cache", 643 | "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'", 644 | "Referrer-Policy": "same-origin", 645 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 646 | "X-Content-Type-Options": "nosniff", 647 | "X-Frame-Options": "DENY", 648 | "X-XSS-Protection": "1; mode=block", 649 | }, 650 | "logLevel": "warn", 651 | "nonceSigningSecret": { 652 | "Fn::GetAtt": [ 653 | "AuthNonceSigningSecretC47F85D8", 654 | "Value", 655 | ], 656 | }, 657 | "oauthScopes": [ 658 | "phone", 659 | "email", 660 | "profile", 661 | "openid", 662 | "aws.cognito.signin.user.admin", 663 | ], 664 | "refreshAuthPath": "/auth/refresh", 665 | "signOutPath": "/auth/sign-out", 666 | "signOutRedirectTo": "/", 667 | "userPoolId": { 668 | "Ref": "UserPool6BA7E5F2", 669 | }, 670 | }, 671 | "FunctionArn": { 672 | "Ref": "SsmParameterValuecfregionuseast1stackStack1c822e4a2b5e248e64ee706fb7d4af8c80d9b03944eCheckAuthFunctionfunctionarnC96584B6F00A464EAD1953AFF4B05118Parameter", 673 | }, 674 | "Nonce": "", 675 | "ServiceToken": { 676 | "Fn::GetAtt": [ 677 | "henristlambdaconfigproviderProviderframeworkonEventB2FC78D4", 678 | "Arn", 679 | ], 680 | }, 681 | }, 682 | "Type": "AWS::CloudFormation::CustomResource", 683 | "UpdateReplacePolicy": "Delete", 684 | }, 685 | "AuthClientSecret768E9C19": { 686 | "DeletionPolicy": "Delete", 687 | "DependsOn": [ 688 | "AuthClientSecretCustomResourcePolicyA4C1D5EC", 689 | ], 690 | "Properties": { 691 | "Create": { 692 | "Fn::Join": [ 693 | "", 694 | [ 695 | "{"service":"CognitoIdentityServiceProvider","action":"describeUserPoolClient","parameters":{"UserPoolId":"", 696 | { 697 | "Ref": "UserPool6BA7E5F2", 698 | }, 699 | "","ClientId":"", 700 | { 701 | "Ref": "UserPoolUserPoolClient40176907", 702 | }, 703 | ""},"physicalResourceId":{"id":"", 704 | { 705 | "Ref": "UserPool6BA7E5F2", 706 | }, 707 | "-", 708 | { 709 | "Ref": "UserPoolUserPoolClient40176907", 710 | }, 711 | ""}}", 712 | ], 713 | ], 714 | }, 715 | "InstallLatestAwsSdk": true, 716 | "ServiceToken": { 717 | "Fn::GetAtt": [ 718 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 719 | "Arn", 720 | ], 721 | }, 722 | "Update": { 723 | "Fn::Join": [ 724 | "", 725 | [ 726 | "{"service":"CognitoIdentityServiceProvider","action":"describeUserPoolClient","parameters":{"UserPoolId":"", 727 | { 728 | "Ref": "UserPool6BA7E5F2", 729 | }, 730 | "","ClientId":"", 731 | { 732 | "Ref": "UserPoolUserPoolClient40176907", 733 | }, 734 | ""},"physicalResourceId":{"id":"", 735 | { 736 | "Ref": "UserPool6BA7E5F2", 737 | }, 738 | "-", 739 | { 740 | "Ref": "UserPoolUserPoolClient40176907", 741 | }, 742 | ""}}", 743 | ], 744 | ], 745 | }, 746 | }, 747 | "Type": "Custom::AWS", 748 | "UpdateReplacePolicy": "Delete", 749 | }, 750 | "AuthClientSecretCustomResourcePolicyA4C1D5EC": { 751 | "Properties": { 752 | "PolicyDocument": { 753 | "Statement": [ 754 | { 755 | "Action": "cognito-idp:DescribeUserPoolClient", 756 | "Effect": "Allow", 757 | "Resource": { 758 | "Fn::GetAtt": [ 759 | "UserPool6BA7E5F2", 760 | "Arn", 761 | ], 762 | }, 763 | }, 764 | ], 765 | "Version": "2012-10-17", 766 | }, 767 | "PolicyName": "AuthClientSecretCustomResourcePolicyA4C1D5EC", 768 | "Roles": [ 769 | { 770 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 771 | }, 772 | ], 773 | }, 774 | "Type": "AWS::IAM::Policy", 775 | }, 776 | "AuthClientUpdate3823E358": { 777 | "DeletionPolicy": "Delete", 778 | "DependsOn": [ 779 | "AuthClientUpdateCustomResourcePolicyF8E9AEC5", 780 | ], 781 | "Properties": { 782 | "Create": { 783 | "Fn::Join": [ 784 | "", 785 | [ 786 | "{"service":"CognitoIdentityServiceProvider","action":"updateUserPoolClient","parameters":{"AllowedOAuthFlows":["code"],"AllowedOAuthFlowsUserPoolClient":true,"SupportedIdentityProviders":["COGNITO"],"AllowedOAuthScopes":["phone","email","profile","openid","aws.cognito.signin.user.admin"],"ClientId":"", 787 | { 788 | "Ref": "UserPoolUserPoolClient40176907", 789 | }, 790 | "","CallbackURLs":["https://", 791 | { 792 | "Fn::GetAtt": [ 793 | "CloudFrontDistributionCFDistribution599ADCC4", 794 | "DomainName", 795 | ], 796 | }, 797 | "/auth/callback"],"LogoutURLs":["https://", 798 | { 799 | "Fn::GetAtt": [ 800 | "CloudFrontDistributionCFDistribution599ADCC4", 801 | "DomainName", 802 | ], 803 | }, 804 | "/"],"UserPoolId":"", 805 | { 806 | "Ref": "UserPool6BA7E5F2", 807 | }, 808 | ""},"physicalResourceId":{"id":"", 809 | { 810 | "Ref": "UserPool6BA7E5F2", 811 | }, 812 | "-", 813 | { 814 | "Ref": "UserPoolUserPoolClient40176907", 815 | }, 816 | ""}}", 817 | ], 818 | ], 819 | }, 820 | "InstallLatestAwsSdk": true, 821 | "ServiceToken": { 822 | "Fn::GetAtt": [ 823 | "AWS679f53fac002430cb0da5b7982bd22872D164C4C", 824 | "Arn", 825 | ], 826 | }, 827 | "Update": { 828 | "Fn::Join": [ 829 | "", 830 | [ 831 | "{"service":"CognitoIdentityServiceProvider","action":"updateUserPoolClient","parameters":{"AllowedOAuthFlows":["code"],"AllowedOAuthFlowsUserPoolClient":true,"SupportedIdentityProviders":["COGNITO"],"AllowedOAuthScopes":["phone","email","profile","openid","aws.cognito.signin.user.admin"],"ClientId":"", 832 | { 833 | "Ref": "UserPoolUserPoolClient40176907", 834 | }, 835 | "","CallbackURLs":["https://", 836 | { 837 | "Fn::GetAtt": [ 838 | "CloudFrontDistributionCFDistribution599ADCC4", 839 | "DomainName", 840 | ], 841 | }, 842 | "/auth/callback"],"LogoutURLs":["https://", 843 | { 844 | "Fn::GetAtt": [ 845 | "CloudFrontDistributionCFDistribution599ADCC4", 846 | "DomainName", 847 | ], 848 | }, 849 | "/"],"UserPoolId":"", 850 | { 851 | "Ref": "UserPool6BA7E5F2", 852 | }, 853 | ""},"physicalResourceId":{"id":"", 854 | { 855 | "Ref": "UserPool6BA7E5F2", 856 | }, 857 | "-", 858 | { 859 | "Ref": "UserPoolUserPoolClient40176907", 860 | }, 861 | ""}}", 862 | ], 863 | ], 864 | }, 865 | }, 866 | "Type": "Custom::AWS", 867 | "UpdateReplacePolicy": "Delete", 868 | }, 869 | "AuthClientUpdateCustomResourcePolicyF8E9AEC5": { 870 | "Properties": { 871 | "PolicyDocument": { 872 | "Statement": [ 873 | { 874 | "Action": "cognito-idp:UpdateUserPoolClient", 875 | "Effect": "Allow", 876 | "Resource": { 877 | "Fn::GetAtt": [ 878 | "UserPool6BA7E5F2", 879 | "Arn", 880 | ], 881 | }, 882 | }, 883 | ], 884 | "Version": "2012-10-17", 885 | }, 886 | "PolicyName": "AuthClientUpdateCustomResourcePolicyF8E9AEC5", 887 | "Roles": [ 888 | { 889 | "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", 890 | }, 891 | ], 892 | }, 893 | "Type": "AWS::IAM::Policy", 894 | }, 895 | "AuthHttpHeadersFn7D541668": { 896 | "DeletionPolicy": "Delete", 897 | "Properties": { 898 | "Config": { 899 | "callbackPath": "/auth/callback", 900 | "clientId": { 901 | "Ref": "UserPoolUserPoolClient40176907", 902 | }, 903 | "clientSecret": { 904 | "Fn::GetAtt": [ 905 | "AuthClientSecret768E9C19", 906 | "UserPoolClient.ClientSecret", 907 | ], 908 | }, 909 | "cognitoAuthDomain": "my-domain.auth.eu-west-1.amazoncognito.com", 910 | "cookieSettings": { 911 | "accessToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 912 | "idToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 913 | "nonce": "Path=/; Secure; HttpOnly; SameSite=Lax", 914 | "refreshToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 915 | }, 916 | "httpHeaders": { 917 | "Cache-Control": "no-cache", 918 | "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'", 919 | "Referrer-Policy": "same-origin", 920 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 921 | "X-Content-Type-Options": "nosniff", 922 | "X-Frame-Options": "DENY", 923 | "X-XSS-Protection": "1; mode=block", 924 | }, 925 | "logLevel": "warn", 926 | "nonceSigningSecret": { 927 | "Fn::GetAtt": [ 928 | "AuthNonceSigningSecretC47F85D8", 929 | "Value", 930 | ], 931 | }, 932 | "oauthScopes": [ 933 | "phone", 934 | "email", 935 | "profile", 936 | "openid", 937 | "aws.cognito.signin.user.admin", 938 | ], 939 | "refreshAuthPath": "/auth/refresh", 940 | "signOutPath": "/auth/sign-out", 941 | "signOutRedirectTo": "/", 942 | "userPoolId": { 943 | "Ref": "UserPool6BA7E5F2", 944 | }, 945 | }, 946 | "FunctionArn": { 947 | "Ref": "SsmParameterValuecfregionuseast1stackStack1c822e4a2b5e248e64ee706fb7d4af8c80d9b03944eHttpHeadersFunctionfunctionarnC96584B6F00A464EAD1953AFF4B05118Parameter", 948 | }, 949 | "Nonce": "", 950 | "ServiceToken": { 951 | "Fn::GetAtt": [ 952 | "henristlambdaconfigproviderProviderframeworkonEventB2FC78D4", 953 | "Arn", 954 | ], 955 | }, 956 | }, 957 | "Type": "AWS::CloudFormation::CustomResource", 958 | "UpdateReplacePolicy": "Delete", 959 | }, 960 | "AuthNonceSigningSecretC47F85D8": { 961 | "DeletionPolicy": "Delete", 962 | "Properties": { 963 | "Nonce": "", 964 | "ServiceToken": { 965 | "Fn::GetAtt": [ 966 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventD98C7326", 967 | "Arn", 968 | ], 969 | }, 970 | }, 971 | "Type": "AWS::CloudFormation::CustomResource", 972 | "UpdateReplacePolicy": "Delete", 973 | }, 974 | "AuthParseAuthFn975D8094": { 975 | "DeletionPolicy": "Delete", 976 | "Properties": { 977 | "Config": { 978 | "callbackPath": "/auth/callback", 979 | "clientId": { 980 | "Ref": "UserPoolUserPoolClient40176907", 981 | }, 982 | "clientSecret": { 983 | "Fn::GetAtt": [ 984 | "AuthClientSecret768E9C19", 985 | "UserPoolClient.ClientSecret", 986 | ], 987 | }, 988 | "cognitoAuthDomain": "my-domain.auth.eu-west-1.amazoncognito.com", 989 | "cookieSettings": { 990 | "accessToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 991 | "idToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 992 | "nonce": "Path=/; Secure; HttpOnly; SameSite=Lax", 993 | "refreshToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 994 | }, 995 | "httpHeaders": { 996 | "Cache-Control": "no-cache", 997 | "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'", 998 | "Referrer-Policy": "same-origin", 999 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 1000 | "X-Content-Type-Options": "nosniff", 1001 | "X-Frame-Options": "DENY", 1002 | "X-XSS-Protection": "1; mode=block", 1003 | }, 1004 | "logLevel": "warn", 1005 | "nonceSigningSecret": { 1006 | "Fn::GetAtt": [ 1007 | "AuthNonceSigningSecretC47F85D8", 1008 | "Value", 1009 | ], 1010 | }, 1011 | "oauthScopes": [ 1012 | "phone", 1013 | "email", 1014 | "profile", 1015 | "openid", 1016 | "aws.cognito.signin.user.admin", 1017 | ], 1018 | "refreshAuthPath": "/auth/refresh", 1019 | "signOutPath": "/auth/sign-out", 1020 | "signOutRedirectTo": "/", 1021 | "userPoolId": { 1022 | "Ref": "UserPool6BA7E5F2", 1023 | }, 1024 | }, 1025 | "FunctionArn": { 1026 | "Ref": "SsmParameterValuecfregionuseast1stackStack1c822e4a2b5e248e64ee706fb7d4af8c80d9b03944eParseAuthFunctionfunctionarnC96584B6F00A464EAD1953AFF4B05118Parameter", 1027 | }, 1028 | "Nonce": "", 1029 | "ServiceToken": { 1030 | "Fn::GetAtt": [ 1031 | "henristlambdaconfigproviderProviderframeworkonEventB2FC78D4", 1032 | "Arn", 1033 | ], 1034 | }, 1035 | }, 1036 | "Type": "AWS::CloudFormation::CustomResource", 1037 | "UpdateReplacePolicy": "Delete", 1038 | }, 1039 | "AuthRefreshAuthFnEA5CBBAE": { 1040 | "DeletionPolicy": "Delete", 1041 | "Properties": { 1042 | "Config": { 1043 | "callbackPath": "/auth/callback", 1044 | "clientId": { 1045 | "Ref": "UserPoolUserPoolClient40176907", 1046 | }, 1047 | "clientSecret": { 1048 | "Fn::GetAtt": [ 1049 | "AuthClientSecret768E9C19", 1050 | "UserPoolClient.ClientSecret", 1051 | ], 1052 | }, 1053 | "cognitoAuthDomain": "my-domain.auth.eu-west-1.amazoncognito.com", 1054 | "cookieSettings": { 1055 | "accessToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 1056 | "idToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 1057 | "nonce": "Path=/; Secure; HttpOnly; SameSite=Lax", 1058 | "refreshToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 1059 | }, 1060 | "httpHeaders": { 1061 | "Cache-Control": "no-cache", 1062 | "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'", 1063 | "Referrer-Policy": "same-origin", 1064 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 1065 | "X-Content-Type-Options": "nosniff", 1066 | "X-Frame-Options": "DENY", 1067 | "X-XSS-Protection": "1; mode=block", 1068 | }, 1069 | "logLevel": "warn", 1070 | "nonceSigningSecret": { 1071 | "Fn::GetAtt": [ 1072 | "AuthNonceSigningSecretC47F85D8", 1073 | "Value", 1074 | ], 1075 | }, 1076 | "oauthScopes": [ 1077 | "phone", 1078 | "email", 1079 | "profile", 1080 | "openid", 1081 | "aws.cognito.signin.user.admin", 1082 | ], 1083 | "refreshAuthPath": "/auth/refresh", 1084 | "signOutPath": "/auth/sign-out", 1085 | "signOutRedirectTo": "/", 1086 | "userPoolId": { 1087 | "Ref": "UserPool6BA7E5F2", 1088 | }, 1089 | }, 1090 | "FunctionArn": { 1091 | "Ref": "SsmParameterValuecfregionuseast1stackStack1c822e4a2b5e248e64ee706fb7d4af8c80d9b03944eRefreshAuthFunctionfunctionarnC96584B6F00A464EAD1953AFF4B05118Parameter", 1092 | }, 1093 | "Nonce": "", 1094 | "ServiceToken": { 1095 | "Fn::GetAtt": [ 1096 | "henristlambdaconfigproviderProviderframeworkonEventB2FC78D4", 1097 | "Arn", 1098 | ], 1099 | }, 1100 | }, 1101 | "Type": "AWS::CloudFormation::CustomResource", 1102 | "UpdateReplacePolicy": "Delete", 1103 | }, 1104 | "AuthSignOutFnD060B75E": { 1105 | "DeletionPolicy": "Delete", 1106 | "Properties": { 1107 | "Config": { 1108 | "callbackPath": "/auth/callback", 1109 | "clientId": { 1110 | "Ref": "UserPoolUserPoolClient40176907", 1111 | }, 1112 | "clientSecret": { 1113 | "Fn::GetAtt": [ 1114 | "AuthClientSecret768E9C19", 1115 | "UserPoolClient.ClientSecret", 1116 | ], 1117 | }, 1118 | "cognitoAuthDomain": "my-domain.auth.eu-west-1.amazoncognito.com", 1119 | "cookieSettings": { 1120 | "accessToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 1121 | "idToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 1122 | "nonce": "Path=/; Secure; HttpOnly; SameSite=Lax", 1123 | "refreshToken": "Path=/; Secure; HttpOnly; SameSite=Lax", 1124 | }, 1125 | "httpHeaders": { 1126 | "Cache-Control": "no-cache", 1127 | "Content-Security-Policy": "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self'", 1128 | "Referrer-Policy": "same-origin", 1129 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", 1130 | "X-Content-Type-Options": "nosniff", 1131 | "X-Frame-Options": "DENY", 1132 | "X-XSS-Protection": "1; mode=block", 1133 | }, 1134 | "logLevel": "warn", 1135 | "nonceSigningSecret": { 1136 | "Fn::GetAtt": [ 1137 | "AuthNonceSigningSecretC47F85D8", 1138 | "Value", 1139 | ], 1140 | }, 1141 | "oauthScopes": [ 1142 | "phone", 1143 | "email", 1144 | "profile", 1145 | "openid", 1146 | "aws.cognito.signin.user.admin", 1147 | ], 1148 | "refreshAuthPath": "/auth/refresh", 1149 | "signOutPath": "/auth/sign-out", 1150 | "signOutRedirectTo": "/", 1151 | "userPoolId": { 1152 | "Ref": "UserPool6BA7E5F2", 1153 | }, 1154 | }, 1155 | "FunctionArn": { 1156 | "Ref": "SsmParameterValuecfregionuseast1stackStack1c822e4a2b5e248e64ee706fb7d4af8c80d9b03944eSignOutFunctionfunctionarnC96584B6F00A464EAD1953AFF4B05118Parameter", 1157 | }, 1158 | "Nonce": "", 1159 | "ServiceToken": { 1160 | "Fn::GetAtt": [ 1161 | "henristlambdaconfigproviderProviderframeworkonEventB2FC78D4", 1162 | "Arn", 1163 | ], 1164 | }, 1165 | }, 1166 | "Type": "AWS::CloudFormation::CustomResource", 1167 | "UpdateReplacePolicy": "Delete", 1168 | }, 1169 | "Bucket83908E77": { 1170 | "DeletionPolicy": "Retain", 1171 | "Type": "AWS::S3::Bucket", 1172 | "UpdateReplacePolicy": "Retain", 1173 | }, 1174 | "CloudFrontDistributionCFDistribution599ADCC4": { 1175 | "Properties": { 1176 | "DistributionConfig": { 1177 | "CacheBehaviors": [ 1178 | { 1179 | "AllowedMethods": [ 1180 | "GET", 1181 | "HEAD", 1182 | ], 1183 | "CachedMethods": [ 1184 | "GET", 1185 | "HEAD", 1186 | ], 1187 | "Compress": true, 1188 | "ForwardedValues": { 1189 | "QueryString": true, 1190 | }, 1191 | "LambdaFunctionAssociations": [ 1192 | { 1193 | "EventType": "viewer-request", 1194 | "LambdaFunctionARN": { 1195 | "Fn::GetAtt": [ 1196 | "AuthParseAuthFn975D8094", 1197 | "FunctionArn", 1198 | ], 1199 | }, 1200 | }, 1201 | ], 1202 | "PathPattern": "/auth/callback", 1203 | "TargetOriginId": "origin1", 1204 | "ViewerProtocolPolicy": "redirect-to-https", 1205 | }, 1206 | { 1207 | "AllowedMethods": [ 1208 | "GET", 1209 | "HEAD", 1210 | ], 1211 | "CachedMethods": [ 1212 | "GET", 1213 | "HEAD", 1214 | ], 1215 | "Compress": true, 1216 | "ForwardedValues": { 1217 | "QueryString": true, 1218 | }, 1219 | "LambdaFunctionAssociations": [ 1220 | { 1221 | "EventType": "viewer-request", 1222 | "LambdaFunctionARN": { 1223 | "Fn::GetAtt": [ 1224 | "AuthRefreshAuthFnEA5CBBAE", 1225 | "FunctionArn", 1226 | ], 1227 | }, 1228 | }, 1229 | ], 1230 | "PathPattern": "/auth/refresh", 1231 | "TargetOriginId": "origin1", 1232 | "ViewerProtocolPolicy": "redirect-to-https", 1233 | }, 1234 | { 1235 | "AllowedMethods": [ 1236 | "GET", 1237 | "HEAD", 1238 | ], 1239 | "CachedMethods": [ 1240 | "GET", 1241 | "HEAD", 1242 | ], 1243 | "Compress": true, 1244 | "ForwardedValues": { 1245 | "QueryString": true, 1246 | }, 1247 | "LambdaFunctionAssociations": [ 1248 | { 1249 | "EventType": "viewer-request", 1250 | "LambdaFunctionARN": { 1251 | "Fn::GetAtt": [ 1252 | "AuthSignOutFnD060B75E", 1253 | "FunctionArn", 1254 | ], 1255 | }, 1256 | }, 1257 | ], 1258 | "PathPattern": "/auth/sign-out", 1259 | "TargetOriginId": "origin1", 1260 | "ViewerProtocolPolicy": "redirect-to-https", 1261 | }, 1262 | ], 1263 | "DefaultCacheBehavior": { 1264 | "AllowedMethods": [ 1265 | "GET", 1266 | "HEAD", 1267 | ], 1268 | "CachedMethods": [ 1269 | "GET", 1270 | "HEAD", 1271 | ], 1272 | "Compress": true, 1273 | "ForwardedValues": { 1274 | "Cookies": { 1275 | "Forward": "none", 1276 | }, 1277 | "QueryString": false, 1278 | }, 1279 | "LambdaFunctionAssociations": [ 1280 | { 1281 | "EventType": "viewer-request", 1282 | "LambdaFunctionARN": { 1283 | "Fn::GetAtt": [ 1284 | "AuthCheckAuthFnC12AAE69", 1285 | "FunctionArn", 1286 | ], 1287 | }, 1288 | }, 1289 | { 1290 | "EventType": "origin-response", 1291 | "LambdaFunctionARN": { 1292 | "Fn::GetAtt": [ 1293 | "AuthHttpHeadersFn7D541668", 1294 | "FunctionArn", 1295 | ], 1296 | }, 1297 | }, 1298 | ], 1299 | "TargetOriginId": "origin1", 1300 | "ViewerProtocolPolicy": "redirect-to-https", 1301 | }, 1302 | "DefaultRootObject": "index.html", 1303 | "Enabled": true, 1304 | "HttpVersion": "http2", 1305 | "IPV6Enabled": true, 1306 | "Origins": [ 1307 | { 1308 | "ConnectionAttempts": 3, 1309 | "ConnectionTimeout": 10, 1310 | "DomainName": { 1311 | "Fn::GetAtt": [ 1312 | "Bucket83908E77", 1313 | "RegionalDomainName", 1314 | ], 1315 | }, 1316 | "Id": "origin1", 1317 | "S3OriginConfig": {}, 1318 | }, 1319 | ], 1320 | "PriceClass": "PriceClass_100", 1321 | "ViewerCertificate": { 1322 | "CloudFrontDefaultCertificate": true, 1323 | }, 1324 | }, 1325 | }, 1326 | "Type": "AWS::CloudFront::Distribution", 1327 | }, 1328 | "UserPool6BA7E5F2": { 1329 | "DeletionPolicy": "Retain", 1330 | "Properties": { 1331 | "AccountRecoverySetting": { 1332 | "RecoveryMechanisms": [ 1333 | { 1334 | "Name": "verified_phone_number", 1335 | "Priority": 1, 1336 | }, 1337 | { 1338 | "Name": "verified_email", 1339 | "Priority": 2, 1340 | }, 1341 | ], 1342 | }, 1343 | "AdminCreateUserConfig": { 1344 | "AllowAdminCreateUserOnly": true, 1345 | }, 1346 | "EmailVerificationMessage": "The verification code to your new account is {####}", 1347 | "EmailVerificationSubject": "Verify your new account", 1348 | "SmsVerificationMessage": "The verification code to your new account is {####}", 1349 | "VerificationMessageTemplate": { 1350 | "DefaultEmailOption": "CONFIRM_WITH_CODE", 1351 | "EmailMessage": "The verification code to your new account is {####}", 1352 | "EmailSubject": "Verify your new account", 1353 | "SmsMessage": "The verification code to your new account is {####}", 1354 | }, 1355 | }, 1356 | "Type": "AWS::Cognito::UserPool", 1357 | "UpdateReplacePolicy": "Retain", 1358 | }, 1359 | "UserPoolUserPoolClient40176907": { 1360 | "Properties": { 1361 | "AllowedOAuthFlows": [ 1362 | "code", 1363 | ], 1364 | "AllowedOAuthFlowsUserPoolClient": true, 1365 | "AllowedOAuthScopes": [ 1366 | "profile", 1367 | "phone", 1368 | "email", 1369 | "openid", 1370 | "aws.cognito.signin.user.admin", 1371 | ], 1372 | "CallbackURLs": [ 1373 | "https://example.com", 1374 | ], 1375 | "ExplicitAuthFlows": [ 1376 | "ALLOW_USER_PASSWORD_AUTH", 1377 | "ALLOW_USER_SRP_AUTH", 1378 | "ALLOW_REFRESH_TOKEN_AUTH", 1379 | ], 1380 | "GenerateSecret": true, 1381 | "PreventUserExistenceErrors": "ENABLED", 1382 | "SupportedIdentityProviders": [ 1383 | "COGNITO", 1384 | ], 1385 | "UserPoolId": { 1386 | "Ref": "UserPool6BA7E5F2", 1387 | }, 1388 | }, 1389 | "Type": "AWS::Cognito::UserPoolClient", 1390 | }, 1391 | "henristcloudfrontauthgeneratesecretproviderFunction3D1AFA99": { 1392 | "DependsOn": [ 1393 | "henristcloudfrontauthgeneratesecretproviderFunctionServiceRole4523A59B", 1394 | ], 1395 | "Properties": { 1396 | "Code": Any, 1397 | "Handler": "index.handler", 1398 | "Role": { 1399 | "Fn::GetAtt": [ 1400 | "henristcloudfrontauthgeneratesecretproviderFunctionServiceRole4523A59B", 1401 | "Arn", 1402 | ], 1403 | }, 1404 | "Runtime": "nodejs16.x", 1405 | }, 1406 | "Type": "AWS::Lambda::Function", 1407 | }, 1408 | "henristcloudfrontauthgeneratesecretproviderFunctionServiceRole4523A59B": { 1409 | "Properties": { 1410 | "AssumeRolePolicyDocument": { 1411 | "Statement": [ 1412 | { 1413 | "Action": "sts:AssumeRole", 1414 | "Effect": "Allow", 1415 | "Principal": { 1416 | "Service": "lambda.amazonaws.com", 1417 | }, 1418 | }, 1419 | ], 1420 | "Version": "2012-10-17", 1421 | }, 1422 | "ManagedPolicyArns": [ 1423 | { 1424 | "Fn::Join": [ 1425 | "", 1426 | [ 1427 | "arn:", 1428 | { 1429 | "Ref": "AWS::Partition", 1430 | }, 1431 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1432 | ], 1433 | ], 1434 | }, 1435 | ], 1436 | }, 1437 | "Type": "AWS::IAM::Role", 1438 | }, 1439 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventD98C7326": { 1440 | "DependsOn": [ 1441 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleDefaultPolicyC126E528", 1442 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleEE714F1C", 1443 | ], 1444 | "Properties": { 1445 | "Code": Any, 1446 | "Description": "AWS CDK resource provider framework - onEvent (Stack2/henrist.cloudfront-auth.generate-secret.provider/Provider)", 1447 | "Environment": { 1448 | "Variables": { 1449 | "USER_ON_EVENT_FUNCTION_ARN": { 1450 | "Fn::GetAtt": [ 1451 | "henristcloudfrontauthgeneratesecretproviderFunction3D1AFA99", 1452 | "Arn", 1453 | ], 1454 | }, 1455 | }, 1456 | }, 1457 | "Handler": "framework.onEvent", 1458 | "Role": { 1459 | "Fn::GetAtt": [ 1460 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleEE714F1C", 1461 | "Arn", 1462 | ], 1463 | }, 1464 | "Runtime": "nodejs14.x", 1465 | "Timeout": 900, 1466 | }, 1467 | "Type": "AWS::Lambda::Function", 1468 | }, 1469 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleDefaultPolicyC126E528": { 1470 | "Properties": { 1471 | "PolicyDocument": { 1472 | "Statement": [ 1473 | { 1474 | "Action": "lambda:InvokeFunction", 1475 | "Effect": "Allow", 1476 | "Resource": [ 1477 | { 1478 | "Fn::GetAtt": [ 1479 | "henristcloudfrontauthgeneratesecretproviderFunction3D1AFA99", 1480 | "Arn", 1481 | ], 1482 | }, 1483 | { 1484 | "Fn::Join": [ 1485 | "", 1486 | [ 1487 | { 1488 | "Fn::GetAtt": [ 1489 | "henristcloudfrontauthgeneratesecretproviderFunction3D1AFA99", 1490 | "Arn", 1491 | ], 1492 | }, 1493 | ":*", 1494 | ], 1495 | ], 1496 | }, 1497 | ], 1498 | }, 1499 | ], 1500 | "Version": "2012-10-17", 1501 | }, 1502 | "PolicyName": "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleDefaultPolicyC126E528", 1503 | "Roles": [ 1504 | { 1505 | "Ref": "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleEE714F1C", 1506 | }, 1507 | ], 1508 | }, 1509 | "Type": "AWS::IAM::Policy", 1510 | }, 1511 | "henristcloudfrontauthgeneratesecretproviderProviderframeworkonEventServiceRoleEE714F1C": { 1512 | "Properties": { 1513 | "AssumeRolePolicyDocument": { 1514 | "Statement": [ 1515 | { 1516 | "Action": "sts:AssumeRole", 1517 | "Effect": "Allow", 1518 | "Principal": { 1519 | "Service": "lambda.amazonaws.com", 1520 | }, 1521 | }, 1522 | ], 1523 | "Version": "2012-10-17", 1524 | }, 1525 | "ManagedPolicyArns": [ 1526 | { 1527 | "Fn::Join": [ 1528 | "", 1529 | [ 1530 | "arn:", 1531 | { 1532 | "Ref": "AWS::Partition", 1533 | }, 1534 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1535 | ], 1536 | ], 1537 | }, 1538 | ], 1539 | }, 1540 | "Type": "AWS::IAM::Role", 1541 | }, 1542 | "henristlambdaconfigproviderProviderframeworkonEventB2FC78D4": { 1543 | "DependsOn": [ 1544 | "henristlambdaconfigproviderProviderframeworkonEventServiceRoleDefaultPolicy3D96FE9E", 1545 | "henristlambdaconfigproviderProviderframeworkonEventServiceRole9AA104AD", 1546 | ], 1547 | "Properties": { 1548 | "Code": Any, 1549 | "Description": "AWS CDK resource provider framework - onEvent (Stack2/henrist.lambda-config.provider/Provider)", 1550 | "Environment": { 1551 | "Variables": { 1552 | "USER_ON_EVENT_FUNCTION_ARN": { 1553 | "Fn::GetAtt": [ 1554 | "henristlambdaconfigproviderUpdateCodeFnBF8DF66B", 1555 | "Arn", 1556 | ], 1557 | }, 1558 | }, 1559 | }, 1560 | "Handler": "framework.onEvent", 1561 | "Role": { 1562 | "Fn::GetAtt": [ 1563 | "henristlambdaconfigproviderProviderframeworkonEventServiceRole9AA104AD", 1564 | "Arn", 1565 | ], 1566 | }, 1567 | "Runtime": "nodejs14.x", 1568 | "Timeout": 900, 1569 | }, 1570 | "Type": "AWS::Lambda::Function", 1571 | }, 1572 | "henristlambdaconfigproviderProviderframeworkonEventServiceRole9AA104AD": { 1573 | "Properties": { 1574 | "AssumeRolePolicyDocument": { 1575 | "Statement": [ 1576 | { 1577 | "Action": "sts:AssumeRole", 1578 | "Effect": "Allow", 1579 | "Principal": { 1580 | "Service": "lambda.amazonaws.com", 1581 | }, 1582 | }, 1583 | ], 1584 | "Version": "2012-10-17", 1585 | }, 1586 | "ManagedPolicyArns": [ 1587 | { 1588 | "Fn::Join": [ 1589 | "", 1590 | [ 1591 | "arn:", 1592 | { 1593 | "Ref": "AWS::Partition", 1594 | }, 1595 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1596 | ], 1597 | ], 1598 | }, 1599 | ], 1600 | }, 1601 | "Type": "AWS::IAM::Role", 1602 | }, 1603 | "henristlambdaconfigproviderProviderframeworkonEventServiceRoleDefaultPolicy3D96FE9E": { 1604 | "Properties": { 1605 | "PolicyDocument": { 1606 | "Statement": [ 1607 | { 1608 | "Action": "lambda:InvokeFunction", 1609 | "Effect": "Allow", 1610 | "Resource": [ 1611 | { 1612 | "Fn::GetAtt": [ 1613 | "henristlambdaconfigproviderUpdateCodeFnBF8DF66B", 1614 | "Arn", 1615 | ], 1616 | }, 1617 | { 1618 | "Fn::Join": [ 1619 | "", 1620 | [ 1621 | { 1622 | "Fn::GetAtt": [ 1623 | "henristlambdaconfigproviderUpdateCodeFnBF8DF66B", 1624 | "Arn", 1625 | ], 1626 | }, 1627 | ":*", 1628 | ], 1629 | ], 1630 | }, 1631 | ], 1632 | }, 1633 | ], 1634 | "Version": "2012-10-17", 1635 | }, 1636 | "PolicyName": "henristlambdaconfigproviderProviderframeworkonEventServiceRoleDefaultPolicy3D96FE9E", 1637 | "Roles": [ 1638 | { 1639 | "Ref": "henristlambdaconfigproviderProviderframeworkonEventServiceRole9AA104AD", 1640 | }, 1641 | ], 1642 | }, 1643 | "Type": "AWS::IAM::Policy", 1644 | }, 1645 | "henristlambdaconfigproviderUpdateCodeFnBF8DF66B": { 1646 | "DependsOn": [ 1647 | "henristlambdaconfigproviderUpdateCodeFnServiceRoleDefaultPolicyE51A664A", 1648 | "henristlambdaconfigproviderUpdateCodeFnServiceRole2FF24712", 1649 | ], 1650 | "Properties": { 1651 | "Code": Any, 1652 | "Handler": "index.handler", 1653 | "Role": { 1654 | "Fn::GetAtt": [ 1655 | "henristlambdaconfigproviderUpdateCodeFnServiceRole2FF24712", 1656 | "Arn", 1657 | ], 1658 | }, 1659 | "Runtime": "nodejs16.x", 1660 | "Timeout": 10, 1661 | }, 1662 | "Type": "AWS::Lambda::Function", 1663 | }, 1664 | "henristlambdaconfigproviderUpdateCodeFnServiceRole2FF24712": { 1665 | "Properties": { 1666 | "AssumeRolePolicyDocument": { 1667 | "Statement": [ 1668 | { 1669 | "Action": "sts:AssumeRole", 1670 | "Effect": "Allow", 1671 | "Principal": { 1672 | "Service": "lambda.amazonaws.com", 1673 | }, 1674 | }, 1675 | ], 1676 | "Version": "2012-10-17", 1677 | }, 1678 | "ManagedPolicyArns": [ 1679 | { 1680 | "Fn::Join": [ 1681 | "", 1682 | [ 1683 | "arn:", 1684 | { 1685 | "Ref": "AWS::Partition", 1686 | }, 1687 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 1688 | ], 1689 | ], 1690 | }, 1691 | ], 1692 | }, 1693 | "Type": "AWS::IAM::Role", 1694 | }, 1695 | "henristlambdaconfigproviderUpdateCodeFnServiceRoleDefaultPolicyE51A664A": { 1696 | "Properties": { 1697 | "PolicyDocument": { 1698 | "Statement": [ 1699 | { 1700 | "Action": [ 1701 | "lambda:GetFunction", 1702 | "lambda:UpdateFunctionCode", 1703 | ], 1704 | "Effect": "Allow", 1705 | "Resource": "*", 1706 | }, 1707 | ], 1708 | "Version": "2012-10-17", 1709 | }, 1710 | "PolicyName": "henristlambdaconfigproviderUpdateCodeFnServiceRoleDefaultPolicyE51A664A", 1711 | "Roles": [ 1712 | { 1713 | "Ref": "henristlambdaconfigproviderUpdateCodeFnServiceRole2FF24712", 1714 | }, 1715 | ], 1716 | }, 1717 | "Type": "AWS::IAM::Policy", 1718 | }, 1719 | }, 1720 | } 1721 | `; 1722 | --------------------------------------------------------------------------------