├── .gitignore ├── .vscode └── settings.json ├── README.md ├── build.sh ├── cdk ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── cdk.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── challenge1 │ │ ├── api │ │ │ ├── functions │ │ │ │ ├── authorizer.ts │ │ │ │ └── policy.ts │ │ │ ├── index.ts │ │ │ ├── integration │ │ │ │ └── lambda.ts │ │ │ └── router │ │ │ │ ├── index.ts │ │ │ │ └── routes │ │ │ │ ├── assets │ │ │ │ └── flag │ │ │ │ ├── base.ts │ │ │ │ ├── flag.ts │ │ │ │ ├── functions │ │ │ │ ├── const.ts │ │ │ │ ├── flag.ts │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── interface.ts │ │ ├── const.ts │ │ ├── deployment.ts │ │ ├── idp │ │ │ └── userPool │ │ │ │ ├── functions │ │ │ │ ├── const.ts │ │ │ │ ├── emailValidate.ts │ │ │ │ └── user.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── website │ │ │ ├── deployment.ts │ │ │ ├── distribution.ts │ │ │ ├── index.ts │ │ │ └── staticBucket.ts │ ├── challenge2 │ │ ├── api │ │ │ ├── functions │ │ │ │ ├── authorizer.ts │ │ │ │ └── policy.ts │ │ │ ├── index.ts │ │ │ ├── integration │ │ │ │ └── lambda.ts │ │ │ └── router │ │ │ │ ├── index.ts │ │ │ │ └── routes │ │ │ │ ├── assets │ │ │ │ └── flag │ │ │ │ ├── base.ts │ │ │ │ ├── flag.ts │ │ │ │ ├── functions │ │ │ │ ├── const.ts │ │ │ │ ├── flag.ts │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── interface.ts │ │ ├── const.ts │ │ ├── deployment.ts │ │ ├── idp │ │ │ └── userPool │ │ │ │ ├── functions │ │ │ │ ├── const.ts │ │ │ │ └── emailValidate.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── website │ │ │ ├── deployment.ts │ │ │ ├── distribution.ts │ │ │ ├── index.ts │ │ │ └── staticBucket.ts │ ├── challenge3 │ │ ├── api │ │ │ ├── functions │ │ │ │ ├── authorizer.ts │ │ │ │ └── policy.ts │ │ │ ├── index.ts │ │ │ ├── integration │ │ │ │ └── lambda.ts │ │ │ └── router │ │ │ │ ├── index.ts │ │ │ │ └── routes │ │ │ │ ├── base.ts │ │ │ │ ├── file.ts │ │ │ │ ├── fileId.ts │ │ │ │ ├── functions │ │ │ │ ├── const.ts │ │ │ │ ├── getFileSign.ts │ │ │ │ ├── index.ts │ │ │ │ ├── listFile.ts │ │ │ │ └── postFile.ts │ │ │ │ ├── index.ts │ │ │ │ └── interface.ts │ │ ├── const.ts │ │ ├── deployment.ts │ │ ├── idp │ │ │ └── userPool │ │ │ │ ├── functions │ │ │ │ ├── const.ts │ │ │ │ ├── emailValidate.ts │ │ │ │ └── user.ts │ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── website │ │ │ ├── assets │ │ │ └── flag │ │ │ ├── assetsBucket.ts │ │ │ ├── deployment.ts │ │ │ ├── distribution.ts │ │ │ ├── index.ts │ │ │ └── staticBucket.ts │ └── challenge4 │ │ ├── api │ │ ├── functions │ │ │ ├── authorizer.ts │ │ │ └── policy.ts │ │ ├── index.ts │ │ ├── integration │ │ │ └── lambda.ts │ │ └── router │ │ │ ├── index.ts │ │ │ └── routes │ │ │ ├── base.ts │ │ │ ├── file.ts │ │ │ ├── fileId.ts │ │ │ ├── functions │ │ │ ├── const.ts │ │ │ ├── getFileSign.ts │ │ │ ├── index.ts │ │ │ ├── listFile.ts │ │ │ └── postFile.ts │ │ │ ├── index.ts │ │ │ └── interface.ts │ │ ├── const.ts │ │ ├── deployment.ts │ │ ├── idp │ │ └── userPool │ │ │ ├── functions │ │ │ ├── const.ts │ │ │ ├── emailValidate.ts │ │ │ └── user.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── website │ │ ├── assets │ │ └── flag │ │ ├── assetsBucket.ts │ │ ├── deployment.ts │ │ ├── distribution.ts │ │ ├── index.ts │ │ └── staticBucket.ts ├── package.json └── tsconfig.json ├── deploy ├── Dockerfile ├── aws │ └── config ├── deploy.sh ├── docker-compose.yml ├── init.sh ├── install.sh └── react-build.sh ├── frontend ├── challenge1 │ ├── Docs.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── challenge2 │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── challenge3 │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── AuthenticatedPage.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json ├── challenge4 │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── AuthenticatedPage.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── reportWebVitals.ts │ │ └── setupTests.ts │ └── tsconfig.json └── react-build.sh └── makefile /.gitignore: -------------------------------------------------------------------------------- 1 | # original 2 | credentials 3 | build 4 | env.json 5 | aws.json 6 | # CDK 7 | *.js 8 | !jest.config.js 9 | *.d.ts 10 | node_modules 11 | .cdk.staging 12 | cdk.out 13 | package-lock.json 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | lerna-debug.log* 21 | .pnpm-debug.log* 22 | 23 | # Diagnostic reports (https://nodejs.org/api/report.html) 24 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | *.lcov 38 | 39 | # nyc test coverage 40 | .nyc_output 41 | 42 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | bower_components 47 | 48 | # node-waf configuration 49 | .lock-wscript 50 | 51 | # Compiled binary addons (https://nodejs.org/api/addons.html) 52 | build/Release 53 | 54 | # Dependency directories 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | web_modules/ 60 | 61 | # TypeScript cache 62 | *.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional stylelint cache 71 | .stylelintcache 72 | 73 | # Microbundle cache 74 | .rpt2_cache/ 75 | .rts2_cache_cjs/ 76 | .rts2_cache_es/ 77 | .rts2_cache_umd/ 78 | 79 | # Optional REPL history 80 | .node_repl_history 81 | 82 | # Output of 'npm pack' 83 | *.tgz 84 | 85 | # Yarn Integrity file 86 | .yarn-integrity 87 | 88 | # dotenv environment variable files 89 | .env 90 | .env.development.local 91 | .env.test.local 92 | .env.production.local 93 | .env.local 94 | 95 | # parcel-bundler cache (https://parceljs.org/) 96 | .cache 97 | .parcel-cache 98 | 99 | # Next.js build output 100 | .next 101 | out 102 | 103 | # Nuxt.js build / generate output 104 | .nuxt 105 | dist 106 | 107 | # Gatsby files 108 | .cache/ 109 | # Comment in the public line in if your project uses Gatsby and not Next.js 110 | # https://nextjs.org/blog/next-9-1#public-directory-support 111 | # public 112 | 113 | # vuepress build output 114 | .vuepress/dist 115 | 116 | # vuepress v2.x temp and cache directory 117 | .temp 118 | .cache 119 | 120 | # Docusaurus cache and generated files 121 | .docusaurus 122 | 123 | # Serverless directories 124 | .serverless/ 125 | 126 | # FuseBox cache 127 | .fusebox/ 128 | 129 | # DynamoDB Local files 130 | .dynamodb/ 131 | 132 | # TernJS port file 133 | .tern-port 134 | 135 | # Stores VSCode versions used for testing VSCode extensions 136 | .vscode-test 137 | 138 | # yarn v2 139 | .yarn/cache 140 | .yarn/unplugged 141 | .yarn/build-state.yml 142 | .yarn/install-state.gz 143 | .pnp.* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["./cdks", "./frontend"] 3 | } 4 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | pushd ./deploy 2 | docker compose build 3 | docker compose run --rm installer bash -c "bash /app/init.sh" 4 | docker compose run --rm react bash -c "npm run build" 5 | popd -------------------------------------------------------------------------------- /cdk/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { "sourceType": "module", "project": "./tsconfig.json" }, 13 | "plugins": ["@typescript-eslint", "prettier"], 14 | "rules": { 15 | "@typescript-eslint/no-explicit-any": 1, 16 | "@typescript-eslint/no-non-null-assertion": 1, 17 | "prettier/prettier": [ 18 | "error", 19 | { 20 | "printWidth": 140, 21 | "tabWidth": 2, 22 | "useTabs": false, 23 | "semi": true, 24 | "singleQuote": true, 25 | "trailingComma": "all", 26 | "bracketSpacing": true, 27 | "arrowParens": "always" 28 | } 29 | ] 30 | }, 31 | "root": true 32 | } 33 | -------------------------------------------------------------------------------- /cdk/.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 | -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /cdk/bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { ApplicationStack as Challenge1ApplicationStack } from '../lib/challenge1/'; 5 | import { ApplicationStack as Challenge2ApplicationStack } from '../lib/challenge2'; 6 | import { ApplicationStack as Challenge3ApplicationStack } from '../lib/challenge3'; 7 | import { ApplicationStack as Challenge4ApplicationStack } from '../lib/challenge4'; 8 | import { DeploymentStack as Challenge1DeploymentStack } from '../lib/challenge1/deployment'; 9 | import { DeploymentStack as Challenge2DeploymentStack } from '../lib/challenge2/deployment'; 10 | import { DeploymentStack as Challenge3DeploymentStack } from '../lib/challenge3/deployment'; 11 | import { DeploymentStack as Challenge4DeploymentStack } from '../lib/challenge4/deployment'; 12 | const app = new cdk.App(); 13 | 14 | /** 15 | * Unique Id 16 | * @description 17 | * uniqueIdは、S3等のグローバルに一意な名前をつける必要があるリソースの名前に利用します。 18 | * @todo 19 | * 環境変数から取得するように変更 20 | */ 21 | const uniqueId = 'DummyIdDummyId'; 22 | const env = { 23 | region: process.env.CDK_DEFAULT_REGION, 24 | }; 25 | 26 | // Challenge1ApplicationStack 27 | const challenge1 = new Challenge1ApplicationStack(app, uniqueId, { env }); 28 | 29 | // Challenge1DeploymentStack 30 | const challenge1Deployment = new Challenge1DeploymentStack(app, { env }); 31 | challenge1Deployment.addDependency(challenge1); 32 | 33 | // Challenge2ApplicationStack 34 | const challenge2 = new Challenge2ApplicationStack(app, uniqueId, { env }); 35 | 36 | // Challenge2DeploymentStack 37 | const challenge2Deployment = new Challenge2DeploymentStack(app, { env }); 38 | challenge2Deployment.addDependency(challenge2); 39 | 40 | // Challenge3ApplicationStack 41 | const challenge3 = new Challenge3ApplicationStack(app, uniqueId, { env }); 42 | 43 | // Challenge3DeploymentStack 44 | const challenge3Deployment = new Challenge3DeploymentStack(app, { env }); 45 | challenge3Deployment.addDependency(challenge3); 46 | 47 | // Challenge4ApplicationStack 48 | const challenge4 = new Challenge4ApplicationStack(app, uniqueId, { env }); 49 | 50 | // Challenge4DeploymentStack 51 | const challenge4Deployment = new Challenge4DeploymentStack(app, { env }); 52 | challenge4Deployment.addDependency(challenge4); 53 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/functions/authorizer.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayAuthorizerHandler, APIGatewayAuthorizerEvent, APIGatewayTokenAuthorizerEvent, APIGatewayRequestAuthorizerEvent, Context } from 'aws-lambda'; 2 | import { CognitoJwtVerifier } from 'aws-jwt-verify'; 3 | import { denyPolicy, allowPolicy } from './policy'; 4 | 5 | const verifier = CognitoJwtVerifier.create({ 6 | userPoolId: process.env.COGNITO_USER_POOL_ID as string, 7 | tokenUse: 'id', 8 | clientId: process.env.COGNITO_USER_POOL_CLIENT_ID as string, 9 | }); 10 | 11 | const tokenAuthorizer = async (event: APIGatewayTokenAuthorizerEvent) => { 12 | const token = event.authorizationToken; 13 | 14 | try { 15 | const payload = await verifier.verify(token); 16 | if (!payload) { 17 | return denyPolicy(event.methodArn, ''); 18 | } 19 | const role = payload['custom:role'] ?? ''; 20 | const context = { 21 | role: role, 22 | }; 23 | return allowPolicy(event.methodArn, context); 24 | } catch (error) { 25 | console.log(error); 26 | return denyPolicy(event.methodArn, ''); 27 | } 28 | }; 29 | 30 | const requestAuthorizer = async (event: APIGatewayRequestAuthorizerEvent) => { 31 | return denyPolicy(event.methodArn, ''); 32 | }; 33 | 34 | export const handler: APIGatewayAuthorizerHandler = (event: APIGatewayAuthorizerEvent, context: Context) => { 35 | if (event.type === 'TOKEN') { 36 | return tokenAuthorizer(event as APIGatewayTokenAuthorizerEvent); 37 | } 38 | return requestAuthorizer(event as APIGatewayRequestAuthorizerEvent); 39 | }; 40 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/functions/policy.ts: -------------------------------------------------------------------------------- 1 | const policy = (sid: string, effect: string, methodArn: string, context: { [key: string]: string } = {}) => { 2 | return { 3 | principalId: '*', 4 | policyDocument: { 5 | Version: '2012-10-17', 6 | Statement: [ 7 | { 8 | Sid: sid, 9 | Action: 'execute-api:Invoke', 10 | Effect: effect, 11 | Resource: methodArn, 12 | }, 13 | ], 14 | }, 15 | context: context, 16 | }; 17 | }; 18 | 19 | export const allowPolicy = (methodArn: string, context: any) => { 20 | return policy('AllowAll', 'Allow', methodArn, context); 21 | }; 22 | export const denyPolicy = (methodArn: string, message: string) => { 23 | return policy('DenyAll: ' + message, 'Deny', methodArn); 24 | }; 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/index.ts: -------------------------------------------------------------------------------- 1 | import { RestApi, RestApiProps, Cors, MethodLoggingLevel, EndpointType, TokenAuthorizer, IdentitySource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { Construct } from 'constructs'; 3 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import * as path from 'path'; 6 | import { APIRoutes } from './router'; 7 | 8 | export interface ApiGatewayProps extends RestApiProps { 9 | origin: string; 10 | userPoolId: string; 11 | userPoolClientId: string; 12 | } 13 | 14 | export class RestApiGateway extends RestApi { 15 | private authorizer: TokenAuthorizer; 16 | private readonly routes: APIRoutes; 17 | private readonly origin: string; 18 | constructor(scope: Construct, id: string, props: ApiGatewayProps) { 19 | super(scope, `${id}Gateway`, { 20 | ...props, 21 | restApiName: `${id}Gateway`, 22 | defaultMethodOptions: {}, 23 | minimumCompressionSize: 1024, 24 | endpointTypes: [EndpointType.REGIONAL], 25 | policy: undefined, 26 | deploy: true, 27 | cloudWatchRole: false, 28 | endpointExportName: undefined, 29 | failOnWarnings: false, 30 | parameters: undefined, 31 | deployOptions: { 32 | stageName: 'v1', 33 | tracingEnabled: true, 34 | loggingLevel: MethodLoggingLevel.INFO, 35 | dataTraceEnabled: true, 36 | metricsEnabled: true, 37 | cacheClusterEnabled: true, 38 | cacheClusterSize: '0.5', 39 | }, 40 | defaultCorsPreflightOptions: { 41 | allowOrigins: [props.origin], 42 | allowMethods: Cors.ALL_METHODS, 43 | allowHeaders: Cors.DEFAULT_HEADERS, 44 | statusCode: 200, 45 | }, 46 | }); 47 | this.origin = props.origin; 48 | this.authorizer = this.createAuthorizer(scope, id, props.userPoolId, props.userPoolClientId); 49 | this.routes = this.createAPIEndpoints(); 50 | } 51 | 52 | get Authorizer() { 53 | return this.authorizer; 54 | } 55 | 56 | get Routes() { 57 | return this.routes; 58 | } 59 | 60 | private createAPIEndpoints() { 61 | return new APIRoutes(this, 'APIRoutes', this.root, { 62 | authorizer: this.authorizer, 63 | originDomain: this.origin || '', 64 | }); 65 | } 66 | 67 | private createAuthorizer(scope: Construct, id: string, userPoolId: string, userPoolClientId: string) { 68 | const entry = path.join(__dirname, 'functions', 'authorizer.ts'); 69 | const authorizer = new lambdaNodejs.NodejsFunction(scope, `${id}AuthorizerFunction`, { 70 | runtime: Runtime.NODEJS_18_X, 71 | entry: entry, 72 | handler: 'handler', 73 | bundling: { 74 | minify: true, 75 | sourceMap: true, 76 | target: 'es2018', 77 | }, 78 | timeout: Duration.seconds(3), 79 | }); 80 | authorizer.addEnvironment('COGNITO_USER_POOL_ID', userPoolId); 81 | authorizer.addEnvironment('COGNITO_USER_POOL_CLIENT_ID', userPoolClientId); 82 | return new TokenAuthorizer(scope, `${id}Authorizer`, { 83 | handler: authorizer, 84 | resultsCacheTtl: Duration.minutes(0), 85 | identitySource: IdentitySource.header('Authorization'), 86 | authorizerName: `${id}Authorizer`, 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/integration/lambda.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Grant } from 'aws-cdk-lib/aws-iam'; 3 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 4 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 5 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 6 | import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway'; 7 | 8 | export class LambdaFunction { 9 | private function: NodejsFunction; 10 | constructor(scope: Construct, id: string, entry: string, environ?: { [key: string]: string }) { 11 | const lambdaFunctionEnviron = environ ? environ : {}; 12 | this.function = new lambdaNodejs.NodejsFunction(scope, id, { 13 | runtime: Runtime.NODEJS_18_X, 14 | entry: entry, 15 | handler: 'handler', 16 | bundling: { 17 | minify: true, 18 | sourceMap: true, 19 | target: 'es2018', 20 | }, 21 | timeout: Duration.seconds(3), 22 | }); 23 | Object.keys(lambdaFunctionEnviron).forEach((key) => { 24 | this.function.addEnvironment(key, lambdaFunctionEnviron[key]); 25 | }); 26 | } 27 | 28 | get Integration(): LambdaIntegration { 29 | return new LambdaIntegration(this.function); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { LambdaFunction } from '../integration/lambda'; 3 | import * as path from 'path'; 4 | import { Resource, IResource, TokenAuthorizer, Method } from 'aws-cdk-lib/aws-apigateway'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import * as fs from 'fs'; 7 | import { IndexRoutes } from './routes'; 8 | import { FlagRoutes } from './routes/flag'; 9 | import { IRoutes } from './routes/interface'; 10 | 11 | export interface RoutesProps { 12 | authorizer: TokenAuthorizer; 13 | originDomain: string; 14 | } 15 | 16 | export class APIRoutes { 17 | readonly resource: Resource | IResource; 18 | readonly authorizer: TokenAuthorizer; 19 | readonly originDomain: string; 20 | readonly scope: Construct; 21 | readonly id: string; 22 | private readonly routes: { [key: string]: { [key: string]: Method } }; 23 | constructor(scope: Construct, id: string, resource: Resource | IResource, props: RoutesProps) { 24 | this.id = id; 25 | this.scope = scope; 26 | this.originDomain = props.originDomain; 27 | this.resource = resource; 28 | this.authorizer = props.authorizer; 29 | this.routes = this.createAPIEndpoints(); 30 | } 31 | get Routes() { 32 | return this.routes; 33 | } 34 | private createAPIEndpoints(): { [key: string]: { [key: string]: Method } } { 35 | const apiResource = this.resource.addResource('api'); 36 | const flagResource = apiResource.addResource('flag'); 37 | const indexRoutes = new IndexRoutes(this.scope, this.id, this.resource, this.originDomain); 38 | const flagRoutes = new FlagRoutes(this.scope, this.id, flagResource, this.originDomain, this.authorizer); 39 | const indexMethod = { 40 | GET: indexRoutes.GET, 41 | }; 42 | const flagMethod = { 43 | GET: flagRoutes.GET, 44 | }; 45 | return { 46 | '/': indexMethod, 47 | '/api/flag': flagMethod, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/assets/flag: -------------------------------------------------------------------------------- 1 | CDK{Challenge1} -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import { IRoutes } from './interface'; 2 | import { Construct } from 'constructs'; 3 | import { Resource, IResource, Method } from 'aws-cdk-lib/aws-apigateway'; 4 | 5 | export class BaseRoutes implements IRoutes { 6 | readonly resource: Resource | IResource; 7 | readonly origin: string; 8 | readonly scope: Construct; 9 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 10 | this.scope = scope; 11 | this.origin = origin; 12 | this.resource = resource; 13 | } 14 | 15 | get GET(): Method | undefined { 16 | return undefined; 17 | } 18 | get POST(): Method | undefined { 19 | return undefined; 20 | } 21 | get PUT(): Method | undefined { 22 | return undefined; 23 | } 24 | get DELETE(): Method | undefined { 25 | return undefined; 26 | } 27 | get PATCH(): Method | undefined { 28 | return undefined; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/flag.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource, TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import { BaseRoutes } from './base'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | 8 | export class FlagRoutes extends BaseRoutes { 9 | private authorizer: TokenAuthorizer; 10 | 11 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string, authorizer: TokenAuthorizer) { 12 | super(scope, id, resource, origin); 13 | this.authorizer = authorizer; 14 | } 15 | override get GET() { 16 | const flag = fs.readFileSync(path.join(__dirname, 'assets', 'flag'), 'utf-8'); 17 | const env = { 18 | ORIGIN_DOMAIN: this.origin, 19 | FLAG: flag ?? 'CDK{this_is_a_fake_flag}', 20 | }; 21 | const integration = new LambdaFunction(this.resource, 'Index', path.join(__dirname, 'functions', 'flag.ts'), env).Integration; 22 | return this.resource.addMethod('GET', integration, { 23 | apiKeyRequired: false, 24 | methodResponses: [ 25 | { 26 | statusCode: '200', 27 | }, 28 | ], 29 | authorizer: this.authorizer, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/functions/const.ts: -------------------------------------------------------------------------------- 1 | export const RESPONSE_BASE_HEADERS = { 2 | 'Content-Type': 'application/json', 3 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 4 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 5 | }; 6 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/functions/flag.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { RESPONSE_BASE_HEADERS } from './const'; 3 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 4 | const headers = { 5 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 6 | ...RESPONSE_BASE_HEADERS, 7 | }; 8 | 9 | const message = event.requestContext.authorizer?.role === 'admin' ? process.env.FLAG : 'You are not "admin" role.'; 10 | 11 | return { 12 | statusCode: 200, 13 | headers, 14 | body: JSON.stringify({ message }), 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { RESPONSE_BASE_HEADERS } from './const'; 3 | 4 | const RESPONSE_HEADERS = { 5 | ...RESPONSE_BASE_HEADERS, 6 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 7 | }; 8 | 9 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 10 | return { 11 | statusCode, 12 | headers: RESPONSE_HEADERS, 13 | body, 14 | }; 15 | }; 16 | 17 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 18 | return response(200, JSON.stringify({ message: 'Hello, world!' })); 19 | }; 20 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import * as path from 'path'; 5 | import { BaseRoutes } from './base'; 6 | export class IndexRoutes extends BaseRoutes { 7 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 8 | super(scope, id, resource, origin); 9 | } 10 | override get GET() { 11 | const env = { 12 | ORIGIN_DOMAIN: this.origin, 13 | }; 14 | const integration = new LambdaFunction(this.resource, 'Index', path.join(__dirname, 'functions', 'index.ts'), env).Integration; 15 | return this.resource.addMethod('GET', integration, { 16 | apiKeyRequired: false, 17 | methodResponses: [ 18 | { 19 | statusCode: '200', 20 | }, 21 | ], 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/api/router/routes/interface.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'aws-cdk-lib/aws-apigateway'; 2 | 3 | export interface IRoutes { 4 | GET?: Method; 5 | POST?: Method; 6 | PUT?: Method; 7 | DELETE?: Method; 8 | PATCH?: Method; 9 | HEAD?: Method; 10 | } 11 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/const.ts: -------------------------------------------------------------------------------- 1 | export const CHALLENGE_NAME = `Challenge1`; 2 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Fn } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { CHALLENGE_NAME } from './const'; 4 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 5 | import { WebSiteDeployment } from './website/deployment'; 6 | 7 | /** 8 | * DeploymentStack 9 | * @class 10 | * @extends Stack 11 | * 12 | * このStackは、Challenge1のためのフロントエンドのデプロイを行います。 13 | * 主にこのスタックで構築されるリソースは以下の通りです。 14 | * - Amazon S3 Bucket 15 | */ 16 | export class DeploymentStack extends Stack { 17 | constructor(scope: Construct, props?: StackProps) { 18 | super(scope, `${CHALLENGE_NAME}DeploymentStack`, props); 19 | const staticBucketName = Fn.importValue(`${CHALLENGE_NAME}WebSiteBucketName`); 20 | const staticBucket = Bucket.fromBucketName(this, `${CHALLENGE_NAME}StaticBucket`, staticBucketName); 21 | new WebSiteDeployment(this, `${CHALLENGE_NAME}WebSiteDeployment`, staticBucket); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/idp/userPool/functions/emailValidate.ts: -------------------------------------------------------------------------------- 1 | import { PreSignUpEmailTriggerEvent } from 'aws-lambda'; 2 | import { invalidEmailDomains } from './const'; 3 | /** 4 | * PreSignUpEmailTrigger 5 | * @param {PreSignUpEmailTriggerEvent} event 6 | * @returns {PreSignUpEmailTriggerEvent} 7 | * 8 | * この関数は、CognitoのPreSignUpEmailTriggerに紐づけられています。 9 | * この関数は、ユーザーがサインアップする際に実行されます。 10 | * 関数ないでは、EmailのAliasが含まれていないことや有名な各種捨てメアドのドメインでないことを確認し、EDoSにつながる可能性のあるユーザーのサインアップを防ぎます。 11 | */ 12 | export const handler = async (event: PreSignUpEmailTriggerEvent) => { 13 | const email = event.request.userAttributes.email; 14 | if (email === undefined) { 15 | throw new Error('Email is not defined'); 16 | } 17 | const emailAlias = email.split('+')[1]; 18 | if (emailAlias !== undefined) { 19 | throw new Error('Email Alias is not allowed'); 20 | } 21 | const emailDomain = email.split('@')[1]; 22 | if (invalidEmailDomains.includes(emailDomain)) { 23 | throw new Error('Email Domain is not allowed'); 24 | } 25 | return event; 26 | }; 27 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/idp/userPool/functions/user.ts: -------------------------------------------------------------------------------- 1 | import { Callback, Context, PostConfirmationTriggerEvent, PostConfirmationTriggerHandler } from 'aws-lambda'; 2 | import { CognitoIdentityProviderClient, AdminUpdateUserAttributesCommand, AdminUpdateUserAttributesCommandInput } from '@aws-sdk/client-cognito-identity-provider'; 3 | 4 | const cognito = new CognitoIdentityProviderClient({ region: 'ap-northeast-1' }); 5 | 6 | /** 7 | * PostConfirmationTrigger 8 | * @param {PostConfirmationTriggerEvent} event 9 | * @param {Context} context 10 | * @param {Callback} callback 11 | * 12 | * この関数は、CognitoのPostConfirmationTriggerに紐づけられています。 13 | * この関数は、ユーザーがサインアップした後に実行されます。 14 | * 関数内では、ユーザーの属性にRoleを追加します。 15 | */ 16 | export const handler: PostConfirmationTriggerHandler = (event: PostConfirmationTriggerEvent, context: Context, callback: Callback) => { 17 | console.log(event); 18 | const updateUserAttributesCommandParam: AdminUpdateUserAttributesCommandInput = { 19 | UserPoolId: event.userPoolId, 20 | Username: event.userName, 21 | UserAttributes: [ 22 | { 23 | Name: 'custom:role', 24 | Value: 'user', 25 | }, 26 | ], 27 | }; 28 | const updateUserAttributesCommand = new AdminUpdateUserAttributesCommand(updateUserAttributesCommandParam); 29 | cognito.send(updateUserAttributesCommand).then((res) => {}); 30 | callback(null, event); 31 | }; 32 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/idp/userPool/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Fn, Duration, RemovalPolicy } from 'aws-cdk-lib'; 3 | import { 4 | UserPool, 5 | UserPoolProps, 6 | AccountRecovery, 7 | Mfa, 8 | VerificationEmailStyle, 9 | StringAttribute, 10 | UserPoolClient, 11 | UserPoolDomain, 12 | ClientAttributes, 13 | StandardAttributesMask, 14 | UserPoolOperation, 15 | } from 'aws-cdk-lib/aws-cognito'; 16 | import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 17 | import { aws_lambda_nodejs as lambdaNodejs } from 'aws-cdk-lib'; 18 | import * as path from 'path'; 19 | 20 | const EMAIL_SUBJECT = 'Verify your email for our Challenge1!'; 21 | const EMAIL_BODY = 'Thanks for signing up to our Challenge1! Your verification code is {####}'; 22 | const CUSTOM_ATTRIBUTES = { 23 | role: new StringAttribute({ mutable: true }), 24 | }; 25 | 26 | export interface Challenge1UserPoolProps extends UserPoolProps { 27 | redirectDomain: string; 28 | } 29 | 30 | export class IdPool extends UserPool { 31 | private userPoolClient: UserPoolClient; 32 | private userPoolDomain: UserPoolDomain; 33 | constructor(scope: Construct, id: string, prorps: Challenge1UserPoolProps) { 34 | const PASSWORD_POLICY = { 35 | minLength: 8, 36 | }; 37 | const SELFE_SIGN_UP_ENABLED = true; 38 | const props: UserPoolProps = { 39 | userPoolName: id, 40 | removalPolicy: RemovalPolicy.DESTROY, 41 | selfSignUpEnabled: SELFE_SIGN_UP_ENABLED, 42 | signInAliases: { 43 | email: true, 44 | }, 45 | autoVerify: { 46 | email: true, 47 | }, 48 | standardAttributes: { 49 | email: { 50 | required: true, 51 | mutable: true, 52 | }, 53 | }, 54 | passwordPolicy: PASSWORD_POLICY, 55 | accountRecovery: AccountRecovery.NONE, 56 | mfa: Mfa.OFF, 57 | userVerification: { 58 | emailSubject: EMAIL_SUBJECT, 59 | emailBody: EMAIL_BODY, 60 | emailStyle: VerificationEmailStyle.CODE, 61 | }, 62 | signInCaseSensitive: false, 63 | customAttributes: CUSTOM_ATTRIBUTES, 64 | }; 65 | super(scope, `${id}UserPool`, props); 66 | const USER_READ_ATTRIBUTES = this.createClientAttributes( 67 | { 68 | email: true, 69 | emailVerified: true, 70 | }, 71 | Object.keys(CUSTOM_ATTRIBUTES), 72 | ); 73 | const USER_WRITE_ATTRIBUTES = this.createClientAttributes( 74 | { 75 | email: true, 76 | }, 77 | Object.keys(CUSTOM_ATTRIBUTES), 78 | ); 79 | this.userPoolClient = this.createClient(`${id}UserPoolClient`, USER_READ_ATTRIBUTES, USER_WRITE_ATTRIBUTES); 80 | this.userPoolDomain = this.createCustomDomain(id); 81 | 82 | this.addPreSignUpTrigger(id); 83 | this.addPostConfirmationTrigger(id); 84 | } 85 | 86 | get client() { 87 | return this.userPoolClient; 88 | } 89 | 90 | get domain() { 91 | return this.userPoolDomain; 92 | } 93 | 94 | private createClient(id: string, readAttributes: ClientAttributes, writeAttributes: ClientAttributes) { 95 | return this.addClient(`${id}Client`, { 96 | userPoolClientName: `${id}Client`, 97 | preventUserExistenceErrors: true, 98 | enableTokenRevocation: true, 99 | refreshTokenValidity: Duration.days(30), 100 | accessTokenValidity: Duration.hours(1), 101 | authFlows: { 102 | userPassword: true, 103 | userSrp: false, 104 | adminUserPassword: false, 105 | custom: false, 106 | }, 107 | readAttributes: readAttributes, 108 | writeAttributes: writeAttributes, 109 | }); 110 | } 111 | 112 | private createClientAttributes(standardAttributes: StandardAttributesMask, customAttributes: string[]) { 113 | return new ClientAttributes().withStandardAttributes(standardAttributes).withCustomAttributes(...customAttributes); 114 | } 115 | 116 | private createCustomDomain(id: string) { 117 | return this.addDomain(`${id}Domain`, { 118 | cognitoDomain: { 119 | domainPrefix: id.toLowerCase(), 120 | }, 121 | }); 122 | } 123 | 124 | private addPreSignUpTrigger(id: string) { 125 | const trigger = this.createTriggerFunction(`${id}PreSignUpTrigger`, 'emailValidate.ts', 10); 126 | this.addTrigger(UserPoolOperation.PRE_SIGN_UP, trigger); 127 | } 128 | 129 | private addPostConfirmationTrigger(id: string) { 130 | const trigger = this.createTriggerFunction(`${id}PostConfirmationTrigger`, 'user.ts', 10); 131 | 132 | const policy = new Policy(this, 'PostConfirmationTriggerPolicy', { 133 | statements: [ 134 | new PolicyStatement({ 135 | actions: ['cognito-idp:AdminUpdateUserAttributes'], 136 | resources: [this.userPoolArn], 137 | }), 138 | ], 139 | }); 140 | trigger.role?.attachInlinePolicy(policy); 141 | this.addTrigger(UserPoolOperation.POST_CONFIRMATION, trigger); 142 | } 143 | 144 | private createTriggerFunction(functionName: string, entry: string, timeout?: number) { 145 | return new lambdaNodejs.NodejsFunction(this, functionName, { 146 | entry: path.join(__dirname, 'functions', entry), 147 | handler: 'handler', 148 | timeout: Duration.seconds(timeout || 3), 149 | bundling: { 150 | minify: true, 151 | sourceMap: true, 152 | target: 'es2018', 153 | }, 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/index.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { IdPool } from './idp/userPool'; 4 | import { WebSite } from './website'; 5 | import { CHALLENGE_NAME } from './const'; 6 | import { RestApiGateway } from './api'; 7 | 8 | /** 9 | * ApplicationStack 10 | * @class 11 | * @extends Stack 12 | * 13 | * このStackは、Challenge1のためのリソースを構築します。 14 | * 主にこのスタックで構築されるリソースは以下の通りです。 15 | * - Amazon S3 Bucket 16 | * - Amazon Cognito User Pool 17 | * - Amazon CloudFront 18 | * - Amazon API Gateway 19 | * - AWS Lambda 20 | */ 21 | export class ApplicationStack extends Stack { 22 | readonly WebSite: WebSite; 23 | readonly IdPool: IdPool; 24 | readonly API: RestApiGateway; 25 | readonly id: string = CHALLENGE_NAME; 26 | constructor(scope: Construct, uniqueId: string, props?: StackProps) { 27 | super(scope, `${CHALLENGE_NAME}ApplicationStack`, props); 28 | this.WebSite = new WebSite(this, this.id, uniqueId); 29 | this.IdPool = new IdPool(this, this.id, { 30 | redirectDomain: this.WebSite.CloudFront.distributionDomainName, 31 | }); 32 | this.API = new RestApiGateway(this, this.id, { 33 | origin: `https://${this.WebSite.CloudFront.distributionDomainName}`, 34 | userPoolId: this.IdPool.userPoolId, 35 | userPoolClientId: this.IdPool.client.userPoolClientId, 36 | }); 37 | this.output(); 38 | } 39 | 40 | output() { 41 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteURL`, { 42 | value: `https://${this.WebSite.CloudFront.distributionDomainName}`, 43 | exportName: `${CHALLENGE_NAME}WebSiteURL`, 44 | }); 45 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteOrigin`, { 46 | value: this.WebSite.CloudFront.distributionDomainName, 47 | exportName: `${CHALLENGE_NAME}WebSiteOrigin`, 48 | }); 49 | 50 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteBucketName`, { 51 | value: this.WebSite.S3Bucket.bucketName, 52 | exportName: `${CHALLENGE_NAME}WebSiteBucketName`, 53 | }); 54 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolId`, { 55 | value: this.IdPool.userPoolId, 56 | exportName: `${CHALLENGE_NAME}UserPoolId`, 57 | }); 58 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolClientId`, { 59 | value: this.IdPool.client.userPoolClientId, 60 | exportName: `${CHALLENGE_NAME}UserPoolClientId`, 61 | }); 62 | new CfnOutput(this, `${CHALLENGE_NAME}APIGatewayURL`, { 63 | value: this.API.url, 64 | exportName: `${CHALLENGE_NAME}APIGatewayURL`, 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cdk/lib/challenge1/website/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 3 | import * as path from 'path'; 4 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 5 | import { CHALLENGE_NAME } from '../const'; 6 | 7 | export class WebSiteDeployment extends BucketDeployment { 8 | constructor(scope: Construct, id: string, destinationBucket: IBucket) { 9 | const assetsPath = path.join('/app', 'frontend', CHALLENGE_NAME.toLowerCase(), 'build'); 10 | console.log(assetsPath); 11 | super(scope, `${id}WebSiteDeployment`, { 12 | destinationBucket, 13 | sources: [ 14 | Source.data('error.html', '

Error

Not Found

{ 12 | const token = event.authorizationToken; 13 | 14 | try { 15 | const payload = await verifier.verify(token); 16 | if (!payload) { 17 | return denyPolicy(event.methodArn, ''); 18 | } 19 | const role = payload['custom:role'] ?? ''; 20 | const context = { 21 | role: role, 22 | }; 23 | return allowPolicy(event.methodArn, context); 24 | } catch (error) { 25 | console.log(error); 26 | return denyPolicy(event.methodArn, ''); 27 | } 28 | }; 29 | 30 | const requestAuthorizer = async (event: APIGatewayRequestAuthorizerEvent) => { 31 | return denyPolicy(event.methodArn, ''); 32 | }; 33 | 34 | export const handler: APIGatewayAuthorizerHandler = (event: APIGatewayAuthorizerEvent, context: Context) => { 35 | if (event.type === 'TOKEN') { 36 | return tokenAuthorizer(event as APIGatewayTokenAuthorizerEvent); 37 | } 38 | return requestAuthorizer(event as APIGatewayRequestAuthorizerEvent); 39 | }; 40 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/functions/policy.ts: -------------------------------------------------------------------------------- 1 | const policy = (sid: string, effect: string, methodArn: string, context: { [key: string]: string } = {}) => { 2 | return { 3 | principalId: '*', 4 | policyDocument: { 5 | Version: '2012-10-17', 6 | Statement: [ 7 | { 8 | Sid: sid, 9 | Action: 'execute-api:Invoke', 10 | Effect: effect, 11 | Resource: methodArn, 12 | }, 13 | ], 14 | }, 15 | context: context, 16 | }; 17 | }; 18 | 19 | export const allowPolicy = (methodArn: string, context: any) => { 20 | return policy('AllowAll', 'Allow', methodArn, context); 21 | }; 22 | export const denyPolicy = (methodArn: string, message: string) => { 23 | return policy('DenyAll: ' + message, 'Deny', methodArn); 24 | }; 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/index.ts: -------------------------------------------------------------------------------- 1 | import { RestApi, RestApiProps, Cors, MethodLoggingLevel, EndpointType, TokenAuthorizer, IdentitySource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { Construct } from 'constructs'; 3 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import * as path from 'path'; 6 | import { APIRoutes } from './router'; 7 | 8 | export interface ApiGatewayProps extends RestApiProps { 9 | origin: string; 10 | userPoolId: string; 11 | userPoolClientId: string; 12 | } 13 | 14 | export class RestApiGateway extends RestApi { 15 | private authorizer: TokenAuthorizer; 16 | private readonly routes: APIRoutes; 17 | private readonly origin: string; 18 | constructor(scope: Construct, id: string, props: ApiGatewayProps) { 19 | super(scope, `${id}Gateway`, { 20 | ...props, 21 | restApiName: `${id}Gateway`, 22 | defaultMethodOptions: {}, 23 | minimumCompressionSize: 1024, 24 | endpointTypes: [EndpointType.REGIONAL], 25 | policy: undefined, 26 | deploy: true, 27 | cloudWatchRole: false, 28 | endpointExportName: undefined, 29 | failOnWarnings: false, 30 | parameters: undefined, 31 | deployOptions: { 32 | stageName: 'v1', 33 | tracingEnabled: true, 34 | loggingLevel: MethodLoggingLevel.INFO, 35 | dataTraceEnabled: true, 36 | metricsEnabled: true, 37 | cacheClusterEnabled: true, 38 | cacheClusterSize: '0.5', 39 | }, 40 | defaultCorsPreflightOptions: { 41 | allowOrigins: [props.origin], 42 | allowMethods: Cors.ALL_METHODS, 43 | allowHeaders: Cors.DEFAULT_HEADERS, 44 | statusCode: 200, 45 | }, 46 | }); 47 | this.origin = props.origin; 48 | this.authorizer = this.createAuthorizer(scope, id, props.userPoolId, props.userPoolClientId); 49 | this.routes = this.createAPIEndpoints(); 50 | } 51 | 52 | get Authorizer() { 53 | return this.authorizer; 54 | } 55 | 56 | get Routes() { 57 | return this.routes; 58 | } 59 | 60 | private createAPIEndpoints() { 61 | return new APIRoutes(this, 'APIRoutes', this.root, { 62 | authorizer: this.authorizer, 63 | originDomain: this.origin || '', 64 | }); 65 | } 66 | 67 | private createAuthorizer(scope: Construct, id: string, userPoolId: string, userPoolClientId: string) { 68 | const entry = path.join(__dirname, 'functions', 'authorizer.ts'); 69 | const authorizer = new lambdaNodejs.NodejsFunction(scope, `${id}AuthorizerFunction`, { 70 | runtime: Runtime.NODEJS_18_X, 71 | entry: entry, 72 | handler: 'handler', 73 | bundling: { 74 | minify: true, 75 | sourceMap: true, 76 | target: 'es2018', 77 | }, 78 | timeout: Duration.seconds(3), 79 | }); 80 | authorizer.addEnvironment('COGNITO_USER_POOL_ID', userPoolId); 81 | authorizer.addEnvironment('COGNITO_USER_POOL_CLIENT_ID', userPoolClientId); 82 | return new TokenAuthorizer(scope, `${id}Authorizer`, { 83 | handler: authorizer, 84 | resultsCacheTtl: Duration.minutes(0), 85 | identitySource: IdentitySource.header('Authorization'), 86 | authorizerName: `${id}Authorizer`, 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/integration/lambda.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Grant } from 'aws-cdk-lib/aws-iam'; 3 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 4 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 5 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 6 | import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway'; 7 | 8 | export class LambdaFunction { 9 | private function: NodejsFunction; 10 | constructor(scope: Construct, id: string, entry: string, environ?: { [key: string]: string }) { 11 | const lambdaFunctionEnviron = environ ? environ : {}; 12 | this.function = new lambdaNodejs.NodejsFunction(scope, id, { 13 | runtime: Runtime.NODEJS_18_X, 14 | entry: entry, 15 | handler: 'handler', 16 | bundling: { 17 | minify: true, 18 | sourceMap: true, 19 | target: 'es2018', 20 | }, 21 | timeout: Duration.seconds(3), 22 | }); 23 | Object.keys(lambdaFunctionEnviron).forEach((key) => { 24 | this.function.addEnvironment(key, lambdaFunctionEnviron[key]); 25 | }); 26 | } 27 | 28 | get Integration(): LambdaIntegration { 29 | return new LambdaIntegration(this.function); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { LambdaFunction } from '../integration/lambda'; 3 | import * as path from 'path'; 4 | import { Resource, IResource, TokenAuthorizer, Method } from 'aws-cdk-lib/aws-apigateway'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import * as fs from 'fs'; 7 | import { IndexRoutes } from './routes'; 8 | import { FlagRoutes } from './routes/flag'; 9 | import { IRoutes } from './routes/interface'; 10 | 11 | export interface RoutesProps { 12 | authorizer: TokenAuthorizer; 13 | originDomain: string; 14 | } 15 | 16 | export class APIRoutes { 17 | readonly resource: Resource | IResource; 18 | readonly authorizer: TokenAuthorizer; 19 | readonly originDomain: string; 20 | readonly scope: Construct; 21 | readonly id: string; 22 | private readonly routes: { [key: string]: { [key: string]: Method } }; 23 | constructor(scope: Construct, id: string, resource: Resource | IResource, props: RoutesProps) { 24 | this.id = id; 25 | this.scope = scope; 26 | this.originDomain = props.originDomain; 27 | this.resource = resource; 28 | this.authorizer = props.authorizer; 29 | this.routes = this.createAPIEndpoints(); 30 | } 31 | get Routes() { 32 | return this.routes; 33 | } 34 | private createAPIEndpoints(): { [key: string]: { [key: string]: Method } } { 35 | const apiResource = this.resource.addResource('api'); 36 | const flagResource = apiResource.addResource('flag'); 37 | const indexRoutes = new IndexRoutes(this.scope, this.id, this.resource, this.originDomain); 38 | const flagRoutes = new FlagRoutes(this.scope, this.id, flagResource, this.originDomain, this.authorizer); 39 | const indexMethod = { 40 | GET: indexRoutes.GET, 41 | }; 42 | const flagMethod = { 43 | GET: flagRoutes.GET, 44 | }; 45 | return { 46 | '/': indexMethod, 47 | '/api/flag': flagMethod, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/assets/flag: -------------------------------------------------------------------------------- 1 | CDK{Challenge2} -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import { IRoutes } from './interface'; 2 | import { Construct } from 'constructs'; 3 | import { Resource, IResource, Method } from 'aws-cdk-lib/aws-apigateway'; 4 | 5 | export class BaseRoutes implements IRoutes { 6 | readonly resource: Resource | IResource; 7 | readonly origin: string; 8 | readonly scope: Construct; 9 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 10 | this.scope = scope; 11 | this.origin = origin; 12 | this.resource = resource; 13 | } 14 | 15 | get GET(): Method | undefined { 16 | return undefined; 17 | } 18 | get POST(): Method | undefined { 19 | return undefined; 20 | } 21 | get PUT(): Method | undefined { 22 | return undefined; 23 | } 24 | get DELETE(): Method | undefined { 25 | return undefined; 26 | } 27 | get PATCH(): Method | undefined { 28 | return undefined; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/flag.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource, TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import { BaseRoutes } from './base'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | 8 | export class FlagRoutes extends BaseRoutes { 9 | private authorizer: TokenAuthorizer; 10 | 11 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string, authorizer: TokenAuthorizer) { 12 | super(scope, id, resource, origin); 13 | this.authorizer = authorizer; 14 | } 15 | override get GET() { 16 | const flag = fs.readFileSync(path.join(__dirname, 'assets', 'flag'), 'utf-8'); 17 | const env = { 18 | ORIGIN_DOMAIN: this.origin, 19 | FLAG: flag ?? 'CDK{this_is_a_fake_flag}', 20 | }; 21 | const integration = new LambdaFunction(this.resource, 'Index', path.join(__dirname, 'functions', 'flag.ts'), env).Integration; 22 | return this.resource.addMethod('GET', integration, { 23 | apiKeyRequired: false, 24 | methodResponses: [ 25 | { 26 | statusCode: '200', 27 | }, 28 | ], 29 | authorizer: this.authorizer, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/functions/const.ts: -------------------------------------------------------------------------------- 1 | export const RESPONSE_BASE_HEADERS = { 2 | 'Content-Type': 'application/json', 3 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 4 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 5 | }; 6 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/functions/flag.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { RESPONSE_BASE_HEADERS } from './const'; 3 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 4 | const headers = { 5 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 6 | ...RESPONSE_BASE_HEADERS, 7 | }; 8 | 9 | const message = event.requestContext.authorizer?.role === 'admin' ? process.env.FLAG : 'You are not "admin" role.'; 10 | 11 | return { 12 | statusCode: 200, 13 | headers, 14 | body: JSON.stringify({ message }), 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { RESPONSE_BASE_HEADERS } from './const'; 3 | 4 | const RESPONSE_HEADERS = { 5 | ...RESPONSE_BASE_HEADERS, 6 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 7 | }; 8 | 9 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 10 | return { 11 | statusCode, 12 | headers: RESPONSE_HEADERS, 13 | body, 14 | }; 15 | }; 16 | 17 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 18 | return response(200, JSON.stringify({ message: 'Hello, world!' })); 19 | }; 20 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import * as path from 'path'; 5 | import { BaseRoutes } from './base'; 6 | export class IndexRoutes extends BaseRoutes { 7 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 8 | super(scope, id, resource, origin); 9 | } 10 | override get GET() { 11 | const env = { 12 | ORIGIN_DOMAIN: this.origin, 13 | }; 14 | const integration = new LambdaFunction(this.resource, 'Index', path.join(__dirname, 'functions', 'index.ts'), env).Integration; 15 | return this.resource.addMethod('GET', integration, { 16 | apiKeyRequired: false, 17 | methodResponses: [ 18 | { 19 | statusCode: '200', 20 | }, 21 | ], 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/api/router/routes/interface.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'aws-cdk-lib/aws-apigateway'; 2 | 3 | export interface IRoutes { 4 | GET?: Method; 5 | POST?: Method; 6 | PUT?: Method; 7 | DELETE?: Method; 8 | PATCH?: Method; 9 | HEAD?: Method; 10 | } 11 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/const.ts: -------------------------------------------------------------------------------- 1 | export const CHALLENGE_NAME = `Challenge2`; 2 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Fn } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { CHALLENGE_NAME } from './const'; 4 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 5 | import { WebSiteDeployment } from './website/deployment'; 6 | 7 | /** 8 | * DeploymentStack 9 | * @class 10 | * @extends Stack 11 | * 12 | * このStackは、Challenge2のためのフロントエンドのデプロイを行います。 13 | * 主にこのスタックで構築されるリソースは以下の通りです。 14 | * - Amazon S3 Bucket 15 | */ 16 | export class DeploymentStack extends Stack { 17 | constructor(scope: Construct, props?: StackProps) { 18 | super(scope, `${CHALLENGE_NAME}DeploymentStack`, props); 19 | const staticBucketName = Fn.importValue(`${CHALLENGE_NAME}WebSiteBucketName`); 20 | const staticBucket = Bucket.fromBucketName(this, `${CHALLENGE_NAME}StaticBucket`, staticBucketName); 21 | new WebSiteDeployment(this, `${CHALLENGE_NAME}WebSiteDeployment`, staticBucket); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/idp/userPool/functions/emailValidate.ts: -------------------------------------------------------------------------------- 1 | import { PreSignUpEmailTriggerEvent } from 'aws-lambda'; 2 | import { invalidEmailDomains } from './const'; 3 | /** 4 | * PreSignUpEmailTrigger 5 | * @param {PreSignUpEmailTriggerEvent} event 6 | * @returns {PreSignUpEmailTriggerEvent} 7 | * 8 | * この関数は、CognitoのPreSignUpEmailTriggerに紐づけられています。 9 | * この関数は、ユーザーがサインアップする際に実行されます。 10 | * 関数ないでは、EmailのAliasが含まれていないことや有名な各種捨てメアドのドメインでないことを確認し、EDoSにつながる可能性のあるユーザーのサインアップを防ぎます。 11 | */ 12 | export const handler = async (event: PreSignUpEmailTriggerEvent) => { 13 | const email = event.request.userAttributes.email; 14 | if (email === undefined) { 15 | throw new Error('Email is not defined'); 16 | } 17 | const emailAlias = email.split('+')[1]; 18 | if (emailAlias !== undefined) { 19 | throw new Error('Email Alias is not allowed'); 20 | } 21 | const emailDomain = email.split('@')[1]; 22 | if (invalidEmailDomains.includes(emailDomain)) { 23 | throw new Error('Email Domain is not allowed'); 24 | } 25 | return event; 26 | }; 27 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/idp/userPool/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Fn, Duration, RemovalPolicy } from 'aws-cdk-lib'; 3 | import { 4 | UserPool, 5 | UserPoolProps, 6 | AccountRecovery, 7 | Mfa, 8 | VerificationEmailStyle, 9 | StringAttribute, 10 | UserPoolClient, 11 | UserPoolDomain, 12 | ClientAttributes, 13 | StandardAttributesMask, 14 | UserPoolOperation, 15 | } from 'aws-cdk-lib/aws-cognito'; 16 | import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 17 | import { aws_lambda_nodejs as lambdaNodejs } from 'aws-cdk-lib'; 18 | import * as path from 'path'; 19 | 20 | const EMAIL_SUBJECT = 'Verify your email for our Challenge1!'; 21 | const EMAIL_BODY = 'Thanks for signing up to our Challenge1! Your verification code is {####}'; 22 | const CUSTOM_ATTRIBUTES = { 23 | role: new StringAttribute({ mutable: true }), 24 | }; 25 | 26 | export interface Challenge1UserPoolProps extends UserPoolProps { 27 | redirectDomain: string; 28 | } 29 | 30 | export class IdPool extends UserPool { 31 | private userPoolClient: UserPoolClient; 32 | private userPoolDomain: UserPoolDomain; 33 | constructor(scope: Construct, id: string, prorps: Challenge1UserPoolProps) { 34 | const PASSWORD_POLICY = { 35 | minLength: 8, 36 | }; 37 | const SELFE_SIGN_UP_ENABLED = true; 38 | const props: UserPoolProps = { 39 | userPoolName: id, 40 | removalPolicy: RemovalPolicy.DESTROY, 41 | selfSignUpEnabled: SELFE_SIGN_UP_ENABLED, 42 | signInAliases: { 43 | email: true, 44 | }, 45 | autoVerify: { 46 | email: true, 47 | }, 48 | standardAttributes: { 49 | email: { 50 | required: true, 51 | mutable: true, 52 | }, 53 | }, 54 | passwordPolicy: PASSWORD_POLICY, 55 | accountRecovery: AccountRecovery.NONE, 56 | mfa: Mfa.OFF, 57 | userVerification: { 58 | emailSubject: EMAIL_SUBJECT, 59 | emailBody: EMAIL_BODY, 60 | emailStyle: VerificationEmailStyle.CODE, 61 | }, 62 | signInCaseSensitive: false, 63 | customAttributes: CUSTOM_ATTRIBUTES, 64 | }; 65 | super(scope, `${id}UserPool`, props); 66 | const USER_READ_ATTRIBUTES = this.createClientAttributes( 67 | { 68 | email: true, 69 | emailVerified: true, 70 | }, 71 | Object.keys(CUSTOM_ATTRIBUTES), 72 | ); 73 | const USER_WRITE_ATTRIBUTES = this.createClientAttributes( 74 | { 75 | email: true, 76 | }, 77 | Object.keys(CUSTOM_ATTRIBUTES), 78 | ); 79 | this.userPoolClient = this.createClient(`${id}UserPoolClient`, USER_READ_ATTRIBUTES, USER_WRITE_ATTRIBUTES); 80 | this.userPoolDomain = this.createCustomDomain(id); 81 | 82 | this.addPreSignUpTrigger(id); 83 | } 84 | 85 | get client() { 86 | return this.userPoolClient; 87 | } 88 | 89 | get domain() { 90 | return this.userPoolDomain; 91 | } 92 | 93 | private createClient(id: string, readAttributes: ClientAttributes, writeAttributes: ClientAttributes) { 94 | return this.addClient(`${id}Client`, { 95 | userPoolClientName: `${id}Client`, 96 | preventUserExistenceErrors: true, 97 | enableTokenRevocation: true, 98 | refreshTokenValidity: Duration.days(30), 99 | accessTokenValidity: Duration.hours(1), 100 | authFlows: { 101 | userPassword: true, 102 | userSrp: false, 103 | adminUserPassword: false, 104 | custom: false, 105 | }, 106 | readAttributes: readAttributes, 107 | writeAttributes: writeAttributes, 108 | }); 109 | } 110 | 111 | private createClientAttributes(standardAttributes: StandardAttributesMask, customAttributes: string[]) { 112 | return new ClientAttributes().withStandardAttributes(standardAttributes).withCustomAttributes(...customAttributes); 113 | } 114 | 115 | private createCustomDomain(id: string) { 116 | return this.addDomain(`${id}Domain`, { 117 | cognitoDomain: { 118 | domainPrefix: id.toLowerCase(), 119 | }, 120 | }); 121 | } 122 | 123 | private addPreSignUpTrigger(id: string) { 124 | const trigger = this.createTriggerFunction(`${id}PreSignUpTrigger`, 'emailValidate.ts', 10); 125 | this.addTrigger(UserPoolOperation.PRE_SIGN_UP, trigger); 126 | } 127 | 128 | private createTriggerFunction(functionName: string, entry: string, timeout?: number) { 129 | return new lambdaNodejs.NodejsFunction(this, functionName, { 130 | entry: path.join(__dirname, 'functions', entry), 131 | handler: 'handler', 132 | timeout: Duration.seconds(timeout || 3), 133 | bundling: { 134 | minify: true, 135 | sourceMap: true, 136 | target: 'es2018', 137 | }, 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/index.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { IdPool } from './idp/userPool'; 4 | import { WebSite } from './website'; 5 | import { CHALLENGE_NAME } from './const'; 6 | import { RestApiGateway } from './api'; 7 | 8 | /** 9 | * ApplicationStack 10 | * @class 11 | * @extends Stack 12 | * 13 | * このStackは、Challenge2のためのリソースを構築します。 14 | * 主にこのスタックで構築されるリソースは以下の通りです。 15 | * - Amazon S3 Bucket 16 | * - Amazon Cognito User Pool 17 | * - Amazon CloudFront 18 | * - Amazon API Gateway 19 | * - AWS Lambda 20 | */ 21 | export class ApplicationStack extends Stack { 22 | readonly WebSite: WebSite; 23 | readonly IdPool: IdPool; 24 | readonly API: RestApiGateway; 25 | readonly id: string = CHALLENGE_NAME; 26 | constructor(scope: Construct, uniqueId: string, props?: StackProps) { 27 | super(scope, `${CHALLENGE_NAME}ApplicationStack`, props); 28 | this.WebSite = new WebSite(this, this.id, uniqueId); 29 | this.IdPool = new IdPool(this, this.id, { 30 | redirectDomain: this.WebSite.CloudFront.distributionDomainName, 31 | }); 32 | this.API = new RestApiGateway(this, this.id, { 33 | origin: `https://${this.WebSite.CloudFront.distributionDomainName}`, 34 | userPoolId: this.IdPool.userPoolId, 35 | userPoolClientId: this.IdPool.client.userPoolClientId, 36 | }); 37 | this.output(); 38 | } 39 | 40 | output() { 41 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteURL`, { 42 | value: `https://${this.WebSite.CloudFront.distributionDomainName}`, 43 | exportName: `${CHALLENGE_NAME}WebSiteURL`, 44 | }); 45 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteOrigin`, { 46 | value: this.WebSite.CloudFront.distributionDomainName, 47 | exportName: `${CHALLENGE_NAME}WebSiteOrigin`, 48 | }); 49 | 50 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteBucketName`, { 51 | value: this.WebSite.S3Bucket.bucketName, 52 | exportName: `${CHALLENGE_NAME}WebSiteBucketName`, 53 | }); 54 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolId`, { 55 | value: this.IdPool.userPoolId, 56 | exportName: `${CHALLENGE_NAME}UserPoolId`, 57 | }); 58 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolClientId`, { 59 | value: this.IdPool.client.userPoolClientId, 60 | exportName: `${CHALLENGE_NAME}UserPoolClientId`, 61 | }); 62 | new CfnOutput(this, `${CHALLENGE_NAME}APIGatewayURL`, { 63 | value: this.API.url, 64 | exportName: `${CHALLENGE_NAME}APIGatewayURL`, 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cdk/lib/challenge2/website/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 3 | import * as path from 'path'; 4 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 5 | import { CHALLENGE_NAME } from '../const'; 6 | 7 | export class WebSiteDeployment extends BucketDeployment { 8 | constructor(scope: Construct, id: string, destinationBucket: IBucket) { 9 | const assetsPath = path.join('/app', 'frontend', CHALLENGE_NAME.toLowerCase(), 'build'); 10 | console.log(assetsPath); 11 | super(scope, `${id}WebSiteDeployment`, { 12 | destinationBucket, 13 | sources: [ 14 | Source.data('error.html', '

Error

Not Found

{ 12 | const token = event.authorizationToken; 13 | 14 | try { 15 | const payload = await verifier.verify(token); 16 | if (!payload) { 17 | return denyPolicy(event.methodArn, ''); 18 | } 19 | const id = payload['custom:id'] ?? ''; 20 | const context = { 21 | id: id, 22 | }; 23 | return allowPolicy(event.methodArn, context); 24 | } catch (error) { 25 | console.log(error); 26 | return denyPolicy(event.methodArn, ''); 27 | } 28 | }; 29 | 30 | const requestAuthorizer = async (event: APIGatewayRequestAuthorizerEvent) => { 31 | return denyPolicy(event.methodArn, ''); 32 | }; 33 | 34 | export const handler: APIGatewayAuthorizerHandler = (event: APIGatewayAuthorizerEvent, context: Context) => { 35 | if (event.type === 'TOKEN') { 36 | return tokenAuthorizer(event as APIGatewayTokenAuthorizerEvent); 37 | } 38 | return requestAuthorizer(event as APIGatewayRequestAuthorizerEvent); 39 | }; 40 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/functions/policy.ts: -------------------------------------------------------------------------------- 1 | const policy = (sid: string, effect: string, methodArn: string, context: { [key: string]: string } = {}) => { 2 | return { 3 | principalId: '*', 4 | policyDocument: { 5 | Version: '2012-10-17', 6 | Statement: [ 7 | { 8 | Sid: sid, 9 | Action: 'execute-api:Invoke', 10 | Effect: effect, 11 | Resource: methodArn, 12 | }, 13 | ], 14 | }, 15 | context: context, 16 | }; 17 | }; 18 | 19 | export const allowPolicy = (methodArn: string, context: any) => { 20 | return policy('AllowAll', 'Allow', methodArn, context); 21 | }; 22 | export const denyPolicy = (methodArn: string, message: string) => { 23 | return policy('DenyAll: ' + message, 'Deny', methodArn); 24 | }; 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/index.ts: -------------------------------------------------------------------------------- 1 | import { RestApi, RestApiProps, Cors, MethodLoggingLevel, EndpointType, TokenAuthorizer, IdentitySource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { Construct } from 'constructs'; 3 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import * as path from 'path'; 6 | import { APIRoutes } from './router'; 7 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 8 | 9 | export interface ApiGatewayProps extends RestApiProps { 10 | origin: string; 11 | userPoolId: string; 12 | userPoolClientId: string; 13 | assetsBucket: Bucket; 14 | } 15 | 16 | export class RestApiGateway extends RestApi { 17 | private authorizer: TokenAuthorizer; 18 | private readonly routes: APIRoutes; 19 | private readonly origin: string; 20 | private readonly assetsBucket: Bucket; 21 | constructor(scope: Construct, id: string, props: ApiGatewayProps) { 22 | super(scope, `${id}Gateway`, { 23 | ...props, 24 | restApiName: `${id}Gateway`, 25 | defaultMethodOptions: {}, 26 | minimumCompressionSize: 1024, 27 | endpointTypes: [EndpointType.REGIONAL], 28 | policy: undefined, 29 | deploy: true, 30 | cloudWatchRole: false, 31 | endpointExportName: undefined, 32 | failOnWarnings: false, 33 | parameters: undefined, 34 | deployOptions: { 35 | stageName: 'v1', 36 | tracingEnabled: true, 37 | loggingLevel: MethodLoggingLevel.INFO, 38 | dataTraceEnabled: true, 39 | metricsEnabled: true, 40 | cacheClusterEnabled: true, 41 | cacheClusterSize: '0.5', 42 | }, 43 | defaultCorsPreflightOptions: { 44 | allowOrigins: [props.origin], 45 | allowMethods: Cors.ALL_METHODS, 46 | allowHeaders: Cors.DEFAULT_HEADERS, 47 | statusCode: 200, 48 | }, 49 | }); 50 | this.origin = props.origin; 51 | this.assetsBucket = props.assetsBucket; 52 | this.authorizer = this.createAuthorizer(scope, id, props.userPoolId, props.userPoolClientId); 53 | this.routes = this.createAPIEndpoints(); 54 | } 55 | 56 | get Authorizer() { 57 | return this.authorizer; 58 | } 59 | 60 | get Routes() { 61 | return this.routes; 62 | } 63 | 64 | private createAPIEndpoints() { 65 | return new APIRoutes(this, 'APIRoutes', this.root, { 66 | authorizer: this.authorizer, 67 | originDomain: this.origin || '', 68 | assetsBucket: this.assetsBucket, 69 | }); 70 | } 71 | 72 | private createAuthorizer(scope: Construct, id: string, userPoolId: string, userPoolClientId: string) { 73 | const entry = path.join(__dirname, 'functions', 'authorizer.ts'); 74 | const authorizer = new lambdaNodejs.NodejsFunction(scope, `${id}AuthorizerFunction`, { 75 | runtime: Runtime.NODEJS_18_X, 76 | entry: entry, 77 | handler: 'handler', 78 | bundling: { 79 | minify: true, 80 | sourceMap: true, 81 | target: 'es2018', 82 | }, 83 | timeout: Duration.seconds(3), 84 | }); 85 | authorizer.addEnvironment('COGNITO_USER_POOL_ID', userPoolId); 86 | authorizer.addEnvironment('COGNITO_USER_POOL_CLIENT_ID', userPoolClientId); 87 | return new TokenAuthorizer(scope, `${id}Authorizer`, { 88 | handler: authorizer, 89 | resultsCacheTtl: Duration.minutes(0), 90 | identitySource: IdentitySource.header('Authorization'), 91 | authorizerName: `${id}Authorizer`, 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/integration/lambda.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Grant } from 'aws-cdk-lib/aws-iam'; 3 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 4 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 5 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 6 | import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway'; 7 | 8 | export class LambdaFunction { 9 | private function: NodejsFunction; 10 | constructor(scope: Construct, id: string, entry: string, environ?: { [key: string]: string }) { 11 | const lambdaFunctionEnviron = environ ? environ : {}; 12 | this.function = new lambdaNodejs.NodejsFunction(scope, id, { 13 | runtime: Runtime.NODEJS_18_X, 14 | entry: entry, 15 | handler: 'handler', 16 | bundling: { 17 | minify: true, 18 | sourceMap: true, 19 | target: 'es2018', 20 | }, 21 | timeout: Duration.seconds(3), 22 | }); 23 | Object.keys(lambdaFunctionEnviron).forEach((key) => { 24 | this.function.addEnvironment(key, lambdaFunctionEnviron[key]); 25 | }); 26 | } 27 | 28 | get Function(): NodejsFunction { 29 | return this.function; 30 | } 31 | 32 | get Integration(): LambdaIntegration { 33 | return new LambdaIntegration(this.function); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { LambdaFunction } from '../integration/lambda'; 3 | import * as path from 'path'; 4 | import { Resource, IResource, TokenAuthorizer, Method } from 'aws-cdk-lib/aws-apigateway'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import * as fs from 'fs'; 7 | import { IndexRoutes } from './routes'; 8 | 9 | import { IRoutes } from './routes/interface'; 10 | import { FileRoutes } from './routes/file'; 11 | import { FileIdRoutes } from './routes/fileId'; 12 | 13 | export interface RoutesProps { 14 | authorizer: TokenAuthorizer; 15 | originDomain: string; 16 | assetsBucket: Bucket; 17 | } 18 | 19 | export class APIRoutes { 20 | readonly resource: Resource | IResource; 21 | readonly authorizer: TokenAuthorizer; 22 | readonly originDomain: string; 23 | readonly scope: Construct; 24 | readonly id: string; 25 | readonly assetsBucket: Bucket; 26 | 27 | private readonly routes: { [key: string]: { [key: string]: Method } }; 28 | constructor(scope: Construct, id: string, resource: Resource | IResource, props: RoutesProps) { 29 | this.id = id; 30 | this.scope = scope; 31 | this.originDomain = props.originDomain; 32 | this.resource = resource; 33 | this.authorizer = props.authorizer; 34 | this.assetsBucket = props.assetsBucket; 35 | this.routes = this.createAPIEndpoints(); 36 | } 37 | get Routes() { 38 | return this.routes; 39 | } 40 | private createAPIEndpoints(): { [key: string]: { [key: string]: Method } } { 41 | const fileResource = this.resource.addResource('api').addResource('file'); 42 | const fileIdResource = fileResource.addResource('{fileId}'); 43 | 44 | const indexRoutes = new IndexRoutes(this.scope, this.id, this.resource, this.originDomain); 45 | const fileRoutes = new FileRoutes(this.scope, this.id, fileResource, this.originDomain, this.authorizer, this.assetsBucket); 46 | const fileIdRoutes = new FileIdRoutes(this.scope, this.id, fileIdResource, this.originDomain, this.authorizer, this.assetsBucket); 47 | 48 | const indexMethod = { 49 | GET: indexRoutes.GET, 50 | }; 51 | 52 | const fileMethod = { 53 | GET: fileRoutes.GET, 54 | POST: fileRoutes.POST, 55 | }; 56 | 57 | const fileIdMethod = { 58 | GET: fileIdRoutes.GET, 59 | }; 60 | 61 | return { 62 | '/': indexMethod, 63 | '/api/file': fileMethod, 64 | '/api/file/{fileId}': fileIdMethod, 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import { IRoutes } from './interface'; 2 | import { Construct } from 'constructs'; 3 | import { Resource, IResource, Method } from 'aws-cdk-lib/aws-apigateway'; 4 | 5 | export class BaseRoutes implements IRoutes { 6 | readonly resource: Resource | IResource; 7 | readonly origin: string; 8 | readonly scope: Construct; 9 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 10 | this.scope = scope; 11 | this.origin = origin; 12 | this.resource = resource; 13 | } 14 | 15 | get GET(): Method | undefined { 16 | return undefined; 17 | } 18 | get POST(): Method | undefined { 19 | return undefined; 20 | } 21 | get PUT(): Method | undefined { 22 | return undefined; 23 | } 24 | get DELETE(): Method | undefined { 25 | return undefined; 26 | } 27 | get PATCH(): Method | undefined { 28 | return undefined; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/file.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource, TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import { BaseRoutes } from './base'; 5 | import * as path from 'path'; 6 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 7 | 8 | export class FileRoutes extends BaseRoutes { 9 | private authorizer: TokenAuthorizer; 10 | private bucket: Bucket; 11 | 12 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string, authorizer: TokenAuthorizer, bucket: Bucket) { 13 | super(scope, id, resource, origin); 14 | this.authorizer = authorizer; 15 | this.bucket = bucket; 16 | } 17 | 18 | private get lambdaEnvironParams() { 19 | return { 20 | ORIGIN_DOMAIN: this.origin, 21 | BUCKET_NAME: this.bucket.bucketName, 22 | }; 23 | } 24 | 25 | override get GET() { 26 | const lambdaFunction = new LambdaFunction(this.resource, 'ListFile', path.join(__dirname, 'functions', 'listFile.ts'), this.lambdaEnvironParams); 27 | this.bucket.grantRead(lambdaFunction.Function); 28 | return this.resource.addMethod('GET', lambdaFunction.Integration, { 29 | apiKeyRequired: false, 30 | methodResponses: [ 31 | { 32 | statusCode: '200', 33 | }, 34 | ], 35 | authorizer: this.authorizer, 36 | }); 37 | } 38 | 39 | override get POST() { 40 | const lambdaFunction = new LambdaFunction(this.resource, 'PostFile', path.join(__dirname, 'functions', 'postFile.ts'), this.lambdaEnvironParams); 41 | this.bucket.grantWrite(lambdaFunction.Function); 42 | return this.resource.addMethod('POST', lambdaFunction.Integration, { 43 | apiKeyRequired: false, 44 | methodResponses: [ 45 | { 46 | statusCode: '200', 47 | }, 48 | ], 49 | authorizer: this.authorizer, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/fileId.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource, TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import { BaseRoutes } from './base'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import * as path from 'path'; 7 | 8 | export class FileIdRoutes extends BaseRoutes { 9 | private authorizer: TokenAuthorizer; 10 | private bucket: Bucket; 11 | 12 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string, authorizer: TokenAuthorizer, bucket: Bucket) { 13 | super(scope, id, resource, origin); 14 | this.authorizer = authorizer; 15 | this.bucket = bucket; 16 | } 17 | 18 | private get lambdaEnvironParams() { 19 | return { 20 | ORIGIN_DOMAIN: this.origin, 21 | BUCKET_NAME: this.bucket.bucketName, 22 | }; 23 | } 24 | 25 | override get GET() { 26 | const lambdaFunction = new LambdaFunction(this.resource, 'GetFileSign', path.join(__dirname, 'functions', 'getFileSign.ts'), this.lambdaEnvironParams); 27 | this.bucket.grantRead(lambdaFunction.Function); 28 | return this.resource.addMethod('GET', lambdaFunction.Integration, { 29 | apiKeyRequired: false, 30 | methodResponses: [ 31 | { 32 | statusCode: '200', 33 | }, 34 | ], 35 | authorizer: this.authorizer, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/functions/const.ts: -------------------------------------------------------------------------------- 1 | export const RESPONSE_BASE_HEADERS = { 2 | 'Content-Type': 'application/json', 3 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 4 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 5 | }; 6 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/functions/getFileSign.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; 3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 4 | import { RESPONSE_BASE_HEADERS } from './const'; 5 | import * as path from 'path'; 6 | 7 | const s3 = new S3Client({ region: 'ap-northeast-1' }); 8 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 9 | const { fileId } = event.pathParameters as { fileId: string }; 10 | const key = path.normalize(`${decodeURIComponent(fileId)}`); 11 | const command = new GetObjectCommand({ 12 | Bucket: process.env.BUCKET_NAME as string, 13 | Key: key, 14 | }); 15 | const url = await getSignedUrl(s3, command, { expiresIn: 60 }); 16 | return { 17 | statusCode: 200, 18 | headers: { 19 | ...RESPONSE_BASE_HEADERS, 20 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 21 | }, 22 | body: JSON.stringify({ 23 | url: url, 24 | }), 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { RESPONSE_BASE_HEADERS } from './const'; 3 | 4 | const RESPONSE_HEADERS = { 5 | ...RESPONSE_BASE_HEADERS, 6 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 7 | }; 8 | 9 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 10 | return { 11 | statusCode, 12 | headers: RESPONSE_HEADERS, 13 | body, 14 | }; 15 | }; 16 | 17 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 18 | return response(200, JSON.stringify({ message: 'Hello, world!' })); 19 | }; 20 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/functions/listFile.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { ListObjectsV2Command, ListObjectsV2CommandInput, S3Client } from '@aws-sdk/client-s3'; 3 | import { RESPONSE_BASE_HEADERS } from './const'; 4 | 5 | const s3 = new S3Client({ region: 'ap-northeast-1' }); 6 | 7 | const getListObjects = async ( 8 | id: string, 9 | nextToken?: string, 10 | ): Promise<{ 11 | Files?: (string | undefined)[]; 12 | NextToken?: string; 13 | }> => { 14 | const listObjectsV2CommandInput: ListObjectsV2CommandInput = nextToken 15 | ? { 16 | Bucket: process.env.BUCKET_NAME as string, 17 | MaxKeys: 10, 18 | ContinuationToken: nextToken, 19 | } 20 | : { 21 | Bucket: process.env.BUCKET_NAME as string, 22 | MaxKeys: 10, 23 | }; 24 | if (id !== '') listObjectsV2CommandInput.Prefix = `${id}/`; 25 | const listObjectsV2Command = new ListObjectsV2Command(listObjectsV2CommandInput); 26 | const listObjectsV2CommandResult = await s3.send(listObjectsV2Command); 27 | 28 | if (!listObjectsV2CommandResult.Contents) return { Files: [] }; 29 | 30 | const files = listObjectsV2CommandResult.Contents.sort((a, b) => { 31 | if (a.LastModified && b.LastModified) { 32 | if (a.LastModified < b.LastModified) return 1; 33 | if (a.LastModified > b.LastModified) return -1; 34 | return 0; 35 | } 36 | return 0; 37 | }) 38 | .filter((content) => { 39 | return content.Key?.split('/').length === 1 || (content.Key?.split('/').length === 2 && content.Key?.split('/')[1] !== ''); 40 | }) 41 | .map((content) => { 42 | return content.Key; 43 | }); 44 | 45 | return { 46 | Files: files, 47 | NextToken: listObjectsV2CommandResult.NextContinuationToken, 48 | }; 49 | }; 50 | 51 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 52 | const { id } = event.requestContext.authorizer as { id: string }; 53 | const { nextToken } = event.queryStringParameters || {}; 54 | 55 | const { Files, NextToken } = await getListObjects(id, nextToken); 56 | 57 | return { 58 | statusCode: 200, 59 | headers: { 60 | ...RESPONSE_BASE_HEADERS, 61 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 62 | }, 63 | body: JSON.stringify({ 64 | files: Files, 65 | nextToken: NextToken ? NextToken : '', 66 | }), 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/functions/postFile.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 4 | import { RESPONSE_BASE_HEADERS } from './const'; 5 | 6 | const s3 = new S3Client({ region: 'ap-northeast-1' }); 7 | 8 | const isAllowedFileSize = (size: number): boolean => { 9 | return 0 <= size && size <= 10 * 1024 * 1024; 10 | }; 11 | 12 | const isAllowedFileName = (fileId: string): boolean => { 13 | return fileId !== undefined && fileId !== '' && !decodeURIComponent(fileId).includes('/'); 14 | }; 15 | 16 | const RESPONSE_HEADERS = { 17 | ...RESPONSE_BASE_HEADERS, 18 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 19 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 20 | }; 21 | 22 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 23 | return { 24 | statusCode, 25 | headers: RESPONSE_HEADERS, 26 | body, 27 | }; 28 | }; 29 | 30 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 31 | const { id } = event.requestContext.authorizer as { id: string }; 32 | const { fileId, size } = JSON.parse(event.body as string) as { fileId: string; size: number }; 33 | 34 | if (!isAllowedFileSize(size)) return response(400, JSON.stringify({ message: 'File size is too large' })); 35 | if (!isAllowedFileName(fileId)) return response(400, JSON.stringify({ message: 'Invalid file name' })); 36 | 37 | const command = new PutObjectCommand({ 38 | Bucket: process.env.BUCKET_NAME as string, 39 | Key: `${id}/${decodeURIComponent(fileId)}`, 40 | ContentType: 'application/octet-stream', 41 | ContentLength: size, 42 | }); 43 | const url = await getSignedUrl(s3, command, { 44 | expiresIn: 60, 45 | }); 46 | 47 | return response(200, JSON.stringify({ url })); 48 | }; 49 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import * as path from 'path'; 5 | import { BaseRoutes } from './base'; 6 | export class IndexRoutes extends BaseRoutes { 7 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 8 | super(scope, id, resource, origin); 9 | } 10 | override get GET() { 11 | const env = { 12 | ORIGIN_DOMAIN: this.origin, 13 | }; 14 | const integration = new LambdaFunction(this.resource, 'Index', path.join(__dirname, 'functions', 'index.ts'), env).Integration; 15 | return this.resource.addMethod('GET', integration, { 16 | apiKeyRequired: false, 17 | methodResponses: [ 18 | { 19 | statusCode: '200', 20 | }, 21 | ], 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/api/router/routes/interface.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'aws-cdk-lib/aws-apigateway'; 2 | 3 | export interface IRoutes { 4 | GET?: Method; 5 | POST?: Method; 6 | PUT?: Method; 7 | DELETE?: Method; 8 | PATCH?: Method; 9 | HEAD?: Method; 10 | } 11 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/const.ts: -------------------------------------------------------------------------------- 1 | export const CHALLENGE_NAME = `Challenge3`; 2 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Fn } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { CHALLENGE_NAME } from './const'; 4 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 5 | import { WebSiteDeployment } from './website/deployment'; 6 | 7 | /** 8 | * DeploymentStack 9 | * @class 10 | * @extends Stack 11 | * 12 | * このStackは、Challenge3のためのフロントエンドのデプロイを行います。 13 | * 主にこのスタックで構築されるリソースは以下の通りです。 14 | * - Amazon S3 Bucket 15 | */ 16 | export class DeploymentStack extends Stack { 17 | constructor(scope: Construct, props?: StackProps) { 18 | super(scope, `${CHALLENGE_NAME}DeploymentStack`, props); 19 | const staticBucketName = Fn.importValue(`${CHALLENGE_NAME}WebSiteBucketName`); 20 | const staticBucket = Bucket.fromBucketName(this, `${CHALLENGE_NAME}StaticBucket`, staticBucketName); 21 | new WebSiteDeployment(this, `${CHALLENGE_NAME}WebSiteDeployment`, staticBucket); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/idp/userPool/functions/emailValidate.ts: -------------------------------------------------------------------------------- 1 | import { PreSignUpEmailTriggerEvent } from 'aws-lambda'; 2 | import { invalidEmailDomains } from './const'; 3 | /** 4 | * PreSignUpEmailTrigger 5 | * @param {PreSignUpEmailTriggerEvent} event 6 | * @returns {PreSignUpEmailTriggerEvent} 7 | * 8 | * この関数は、CognitoのPreSignUpEmailTriggerに紐づけられています。 9 | * この関数は、ユーザーがサインアップする際に実行されます。 10 | * 関数ないでは、EmailのAliasが含まれていないことや有名な各種捨てメアドのドメインでないことを確認し、EDoSにつながる可能性のあるユーザーのサインアップを防ぎます。 11 | */ 12 | export const handler = async (event: PreSignUpEmailTriggerEvent) => { 13 | const email = event.request.userAttributes.email; 14 | if (email === undefined) { 15 | throw new Error('Email is not defined'); 16 | } 17 | const emailAlias = email.split('+')[1]; 18 | if (emailAlias !== undefined) { 19 | throw new Error('Email Alias is not allowed'); 20 | } 21 | const emailDomain = email.split('@')[1]; 22 | if (invalidEmailDomains.includes(emailDomain)) { 23 | throw new Error('Email Domain is not allowed'); 24 | } 25 | return event; 26 | }; 27 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/idp/userPool/functions/user.ts: -------------------------------------------------------------------------------- 1 | import { Callback, Context, PostConfirmationTriggerEvent, PostConfirmationTriggerHandler } from 'aws-lambda'; 2 | import { CognitoIdentityProviderClient, AdminUpdateUserAttributesCommand, AdminUpdateUserAttributesCommandInput } from '@aws-sdk/client-cognito-identity-provider'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | const cognito = new CognitoIdentityProviderClient({ region: 'ap-northeast-1' }); 5 | 6 | /** 7 | * PostConfirmationTrigger 8 | * @param {PostConfirmationTriggerEvent} event 9 | * @param {Context} context 10 | * @param {Callback} callback 11 | * 12 | * この関数は、CognitoのPostConfirmationTriggerに紐づけられています。 13 | * この関数は、ユーザーがサインアップした後に実行されます。 14 | * 関数内では、ユーザーの属性にアプリケーション独自のUserのIdを追加します 15 | */ 16 | export const handler: PostConfirmationTriggerHandler = (event: PostConfirmationTriggerEvent, context: Context, callback: Callback) => { 17 | console.log(event); 18 | const userId = uuidv4(); 19 | const updateUserAttributesCommandParam: AdminUpdateUserAttributesCommandInput = { 20 | UserPoolId: event.userPoolId, 21 | Username: event.userName, 22 | UserAttributes: [ 23 | { 24 | Name: 'custom:id', 25 | Value: userId, 26 | }, 27 | ], 28 | }; 29 | const updateUserAttributesCommand = new AdminUpdateUserAttributesCommand(updateUserAttributesCommandParam); 30 | cognito.send(updateUserAttributesCommand).then((res) => {}); 31 | callback(null, event); 32 | }; 33 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/idp/userPool/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Fn, Duration, RemovalPolicy } from 'aws-cdk-lib'; 3 | import { 4 | UserPool, 5 | UserPoolProps, 6 | AccountRecovery, 7 | Mfa, 8 | VerificationEmailStyle, 9 | StringAttribute, 10 | UserPoolClient, 11 | UserPoolDomain, 12 | ClientAttributes, 13 | StandardAttributesMask, 14 | UserPoolOperation, 15 | } from 'aws-cdk-lib/aws-cognito'; 16 | import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 17 | import { aws_lambda_nodejs as lambdaNodejs } from 'aws-cdk-lib'; 18 | import * as path from 'path'; 19 | 20 | const EMAIL_SUBJECT = 'Verify your email for our Challenge3!'; 21 | const EMAIL_BODY = 'Thanks for signing up to our Challenge3! Your verification code is {####}'; 22 | const CUSTOM_ATTRIBUTES = { 23 | id: new StringAttribute({ mutable: true }), 24 | }; 25 | 26 | export interface Challenge1UserPoolProps extends UserPoolProps { 27 | redirectDomain: string; 28 | } 29 | 30 | export class IdPool extends UserPool { 31 | private userPoolClient: UserPoolClient; 32 | private userPoolDomain: UserPoolDomain; 33 | constructor(scope: Construct, id: string, prorps: Challenge1UserPoolProps) { 34 | const PASSWORD_POLICY = { 35 | minLength: 8, 36 | }; 37 | const SELFE_SIGN_UP_ENABLED = true; 38 | const props: UserPoolProps = { 39 | userPoolName: id, 40 | removalPolicy: RemovalPolicy.DESTROY, 41 | selfSignUpEnabled: SELFE_SIGN_UP_ENABLED, 42 | signInAliases: { 43 | email: true, 44 | }, 45 | autoVerify: { 46 | email: true, 47 | }, 48 | standardAttributes: { 49 | email: { 50 | required: true, 51 | mutable: true, 52 | }, 53 | }, 54 | passwordPolicy: PASSWORD_POLICY, 55 | accountRecovery: AccountRecovery.NONE, 56 | mfa: Mfa.OFF, 57 | userVerification: { 58 | emailSubject: EMAIL_SUBJECT, 59 | emailBody: EMAIL_BODY, 60 | emailStyle: VerificationEmailStyle.CODE, 61 | }, 62 | signInCaseSensitive: false, 63 | customAttributes: CUSTOM_ATTRIBUTES, 64 | }; 65 | super(scope, `${id}UserPool`, props); 66 | const USER_READ_ATTRIBUTES = this.createClientAttributes( 67 | { 68 | email: true, 69 | emailVerified: true, 70 | }, 71 | Object.keys(CUSTOM_ATTRIBUTES), 72 | ); 73 | const USER_WRITE_ATTRIBUTES = this.createClientAttributes( 74 | { 75 | email: true, 76 | }, 77 | Object.keys(CUSTOM_ATTRIBUTES), 78 | ); 79 | this.userPoolClient = this.createClient(`${id}UserPoolClient`, USER_READ_ATTRIBUTES, USER_WRITE_ATTRIBUTES); 80 | this.userPoolDomain = this.createCustomDomain(id); 81 | 82 | this.addPreSignUpTrigger(id); 83 | this.addPostConfirmationTrigger(id); 84 | } 85 | 86 | get client() { 87 | return this.userPoolClient; 88 | } 89 | 90 | get domain() { 91 | return this.userPoolDomain; 92 | } 93 | 94 | private createClient(id: string, readAttributes: ClientAttributes, writeAttributes: ClientAttributes) { 95 | return this.addClient(`${id}Client`, { 96 | userPoolClientName: `${id}Client`, 97 | preventUserExistenceErrors: true, 98 | enableTokenRevocation: true, 99 | refreshTokenValidity: Duration.days(30), 100 | accessTokenValidity: Duration.hours(1), 101 | authFlows: { 102 | userPassword: true, 103 | userSrp: false, 104 | adminUserPassword: false, 105 | custom: false, 106 | }, 107 | readAttributes: readAttributes, 108 | writeAttributes: writeAttributes, 109 | }); 110 | } 111 | 112 | private createClientAttributes(standardAttributes: StandardAttributesMask, customAttributes: string[]) { 113 | return new ClientAttributes().withStandardAttributes(standardAttributes).withCustomAttributes(...customAttributes); 114 | } 115 | 116 | private createCustomDomain(id: string) { 117 | return this.addDomain(`${id}Domain`, { 118 | cognitoDomain: { 119 | domainPrefix: id.toLowerCase(), 120 | }, 121 | }); 122 | } 123 | 124 | private addPreSignUpTrigger(id: string) { 125 | const trigger = this.createTriggerFunction(`${id}PreSignUpTrigger`, 'emailValidate.ts', 10); 126 | this.addTrigger(UserPoolOperation.PRE_SIGN_UP, trigger); 127 | } 128 | 129 | private addPostConfirmationTrigger(id: string) { 130 | const trigger = this.createTriggerFunction(`${id}PostConfirmationTrigger`, 'user.ts', 10); 131 | 132 | const policy = new Policy(this, 'PostConfirmationTriggerPolicy', { 133 | statements: [ 134 | new PolicyStatement({ 135 | actions: ['cognito-idp:AdminUpdateUserAttributes'], 136 | resources: [this.userPoolArn], 137 | }), 138 | ], 139 | }); 140 | trigger.role?.attachInlinePolicy(policy); 141 | this.addTrigger(UserPoolOperation.POST_CONFIRMATION, trigger); 142 | } 143 | 144 | private createTriggerFunction(functionName: string, entry: string, timeout?: number) { 145 | return new lambdaNodejs.NodejsFunction(this, functionName, { 146 | entry: path.join(__dirname, 'functions', entry), 147 | handler: 'handler', 148 | timeout: Duration.seconds(timeout || 3), 149 | bundling: { 150 | minify: true, 151 | sourceMap: true, 152 | target: 'es2018', 153 | }, 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/index.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { IdPool } from './idp/userPool'; 4 | import { WebSite } from './website'; 5 | import { CHALLENGE_NAME } from './const'; 6 | import { RestApiGateway } from './api'; 7 | 8 | /** 9 | * ApplicationStack 10 | * @class 11 | * @extends Stack 12 | * 13 | * このStackは、Challenge3のためのリソースを構築します。 14 | * 主にこのスタックで構築されるリソースは以下の通りです。 15 | * - Amazon S3 Bucket 16 | * - Amazon Cognito User Pool 17 | * - Amazon CloudFront 18 | * - Amazon API Gateway 19 | * - AWS Lambda 20 | */ 21 | export class ApplicationStack extends Stack { 22 | readonly WebSite: WebSite; 23 | readonly IdPool: IdPool; 24 | readonly API: RestApiGateway; 25 | readonly id: string = CHALLENGE_NAME; 26 | constructor(scope: Construct, uniqueId: string, props?: StackProps) { 27 | super(scope, `${CHALLENGE_NAME}ApplicationStack`, props); 28 | this.WebSite = new WebSite(this, this.id, uniqueId); 29 | this.IdPool = new IdPool(this, this.id, { 30 | redirectDomain: this.WebSite.CloudFront.distributionDomainName, 31 | }); 32 | this.API = new RestApiGateway(this, this.id, { 33 | origin: `https://${this.WebSite.CloudFront.distributionDomainName}`, 34 | userPoolId: this.IdPool.userPoolId, 35 | userPoolClientId: this.IdPool.client.userPoolClientId, 36 | assetsBucket: this.WebSite.AssetsS3Bucket, 37 | }); 38 | this.output(); 39 | } 40 | 41 | output() { 42 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteURL`, { 43 | value: `https://${this.WebSite.CloudFront.distributionDomainName}`, 44 | exportName: `${CHALLENGE_NAME}WebSiteURL`, 45 | }); 46 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteOrigin`, { 47 | value: this.WebSite.CloudFront.distributionDomainName, 48 | exportName: `${CHALLENGE_NAME}WebSiteOrigin`, 49 | }); 50 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteBucketName`, { 51 | value: this.WebSite.StaticWebS3Bucket.bucketName, 52 | exportName: `${CHALLENGE_NAME}WebSiteBucketName`, 53 | }); 54 | new CfnOutput(this, `${CHALLENGE_NAME}AssetsBucketName`, { 55 | value: this.WebSite.AssetsS3Bucket.bucketName, 56 | exportName: `${CHALLENGE_NAME}AssetsBucketName`, 57 | }); 58 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolId`, { 59 | value: this.IdPool.userPoolId, 60 | exportName: `${CHALLENGE_NAME}UserPoolId`, 61 | }); 62 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolClientId`, { 63 | value: this.IdPool.client.userPoolClientId, 64 | exportName: `${CHALLENGE_NAME}UserPoolClientId`, 65 | }); 66 | new CfnOutput(this, `${CHALLENGE_NAME}APIGatewayURL`, { 67 | value: this.API.url, 68 | exportName: `${CHALLENGE_NAME}APIGatewayURL`, 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/website/assets/flag: -------------------------------------------------------------------------------- 1 | CDK{Challenge3} -------------------------------------------------------------------------------- /cdk/lib/challenge3/website/assetsBucket.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy } from 'aws-cdk-lib'; 2 | import { Bucket, BucketProps, BucketEncryption, HttpMethods } from 'aws-cdk-lib/aws-s3'; 3 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 4 | import { Construct } from 'constructs'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | 8 | export interface AssetsBucketProps extends BucketProps { 9 | origin: string; 10 | } 11 | 12 | /** 13 | * AssetsBucket 14 | * @class 15 | * @extends Bucket 16 | * 17 | * このクラスは、アップロードされたファイルを保存するためのS3バケットを構築します。 18 | */ 19 | export class AssetsBucket extends Bucket { 20 | constructor(scope: Construct, id: string, props: AssetsBucketProps) { 21 | super(scope, id, { 22 | ...props, 23 | bucketName: `${id.toLowerCase()}-assets-bucket`, 24 | encryption: BucketEncryption.S3_MANAGED, 25 | removalPolicy: RemovalPolicy.DESTROY, 26 | autoDeleteObjects: true, 27 | lifecycleRules: [], 28 | cors: [ 29 | { 30 | allowedMethods: [HttpMethods.GET, HttpMethods.HEAD, HttpMethods.PUT], 31 | allowedOrigins: [`https://${props.origin}`], 32 | allowedHeaders: ['*'], 33 | }, 34 | ], 35 | publicReadAccess: false, 36 | blockPublicAccess: { 37 | blockPublicAcls: true, 38 | blockPublicPolicy: false, 39 | ignorePublicAcls: true, 40 | restrictPublicBuckets: false, 41 | }, 42 | }); 43 | if (!props) return; 44 | this.defaultAssetsDeploy(scope); 45 | } 46 | 47 | private defaultAssetsDeploy(scope: Construct) { 48 | const flag = fs.readFileSync(path.join(__dirname, 'assets', 'flag'), 'utf-8'); 49 | const sources = [Source.data('flag.txt', flag)]; 50 | new BucketDeployment(scope, 'AssetsBucketDeployment', { 51 | destinationBucket: this, 52 | sources: sources, 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cdk/lib/challenge3/website/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 3 | import * as path from 'path'; 4 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 5 | import { CHALLENGE_NAME } from '../const'; 6 | 7 | export class WebSiteDeployment extends BucketDeployment { 8 | constructor(scope: Construct, id: string, destinationBucket: IBucket) { 9 | const assetsPath = path.join('/app', 'frontend', CHALLENGE_NAME.toLowerCase(), 'build'); 10 | console.log(assetsPath); 11 | super(scope, `${id}WebSiteDeployment`, { 12 | destinationBucket, 13 | sources: [ 14 | Source.data('error.html', '

Error

Not Found

{ 12 | const token = event.authorizationToken; 13 | 14 | try { 15 | const payload = await verifier.verify(token); 16 | if (!payload) { 17 | return denyPolicy(event.methodArn, ''); 18 | } 19 | const id = payload['custom:id'] ?? ''; 20 | const context = { 21 | id: id, 22 | }; 23 | return allowPolicy(event.methodArn, context); 24 | } catch (error) { 25 | console.log(error); 26 | return denyPolicy(event.methodArn, ''); 27 | } 28 | }; 29 | 30 | const requestAuthorizer = async (event: APIGatewayRequestAuthorizerEvent) => { 31 | return denyPolicy(event.methodArn, ''); 32 | }; 33 | 34 | export const handler: APIGatewayAuthorizerHandler = (event: APIGatewayAuthorizerEvent, context: Context) => { 35 | if (event.type === 'TOKEN') { 36 | return tokenAuthorizer(event as APIGatewayTokenAuthorizerEvent); 37 | } 38 | return requestAuthorizer(event as APIGatewayRequestAuthorizerEvent); 39 | }; 40 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/functions/policy.ts: -------------------------------------------------------------------------------- 1 | const policy = (sid: string, effect: string, methodArn: string, context: { [key: string]: string } = {}) => { 2 | return { 3 | principalId: '*', 4 | policyDocument: { 5 | Version: '2012-10-17', 6 | Statement: [ 7 | { 8 | Sid: sid, 9 | Action: 'execute-api:Invoke', 10 | Effect: effect, 11 | Resource: methodArn, 12 | }, 13 | ], 14 | }, 15 | context: context, 16 | }; 17 | }; 18 | 19 | export const allowPolicy = (methodArn: string, context: any) => { 20 | return policy('AllowAll', 'Allow', methodArn, context); 21 | }; 22 | export const denyPolicy = (methodArn: string, message: string) => { 23 | return policy('DenyAll: ' + message, 'Deny', methodArn); 24 | }; 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/index.ts: -------------------------------------------------------------------------------- 1 | import { RestApi, RestApiProps, Cors, MethodLoggingLevel, EndpointType, TokenAuthorizer, IdentitySource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { Construct } from 'constructs'; 3 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import * as path from 'path'; 6 | import { APIRoutes } from './router'; 7 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 8 | 9 | export interface ApiGatewayProps extends RestApiProps { 10 | origin: string; 11 | assetsBucket: Bucket; 12 | userPoolId: string; 13 | userPoolClientId: string; 14 | } 15 | 16 | export class RestApiGateway extends RestApi { 17 | private authorizer: TokenAuthorizer; 18 | private readonly routes: APIRoutes; 19 | private readonly origin: string; 20 | private readonly assetsBucket: Bucket; 21 | constructor(scope: Construct, id: string, props: ApiGatewayProps) { 22 | super(scope, `${id}Gateway`, { 23 | ...props, 24 | restApiName: `${id}Gateway`, 25 | defaultMethodOptions: {}, 26 | minimumCompressionSize: 1024, 27 | endpointTypes: [EndpointType.REGIONAL], 28 | policy: undefined, 29 | deploy: true, 30 | cloudWatchRole: false, 31 | endpointExportName: undefined, 32 | failOnWarnings: false, 33 | parameters: undefined, 34 | deployOptions: { 35 | stageName: 'v1', 36 | tracingEnabled: true, 37 | loggingLevel: MethodLoggingLevel.INFO, 38 | dataTraceEnabled: true, 39 | metricsEnabled: true, 40 | cacheClusterEnabled: true, 41 | cacheClusterSize: '0.5', 42 | }, 43 | defaultCorsPreflightOptions: { 44 | allowOrigins: [props.origin], 45 | allowMethods: Cors.ALL_METHODS, 46 | allowHeaders: Cors.DEFAULT_HEADERS, 47 | statusCode: 200, 48 | }, 49 | }); 50 | this.origin = props.origin; 51 | this.assetsBucket = props.assetsBucket; 52 | this.authorizer = this.createAuthorizer(scope, id, props.userPoolId, props.userPoolClientId); 53 | this.routes = this.createAPIEndpoints(); 54 | } 55 | 56 | get Authorizer() { 57 | return this.authorizer; 58 | } 59 | 60 | get Routes() { 61 | return this.routes; 62 | } 63 | 64 | private createAPIEndpoints() { 65 | return new APIRoutes(this, 'APIRoutes', this.root, { 66 | authorizer: this.authorizer, 67 | originDomain: this.origin || '', 68 | assetsBucket: this.assetsBucket, 69 | }); 70 | } 71 | 72 | private createAuthorizer(scope: Construct, id: string, userPoolId: string, userPoolClientId: string) { 73 | const entry = path.join(__dirname, 'functions', 'authorizer.ts'); 74 | const authorizer = new lambdaNodejs.NodejsFunction(scope, `${id}AuthorizerFunction`, { 75 | runtime: Runtime.NODEJS_18_X, 76 | entry: entry, 77 | handler: 'handler', 78 | bundling: { 79 | minify: true, 80 | sourceMap: true, 81 | target: 'es2018', 82 | }, 83 | timeout: Duration.seconds(3), 84 | }); 85 | authorizer.addEnvironment('COGNITO_USER_POOL_ID', userPoolId); 86 | authorizer.addEnvironment('COGNITO_USER_POOL_CLIENT_ID', userPoolClientId); 87 | return new TokenAuthorizer(scope, `${id}Authorizer`, { 88 | handler: authorizer, 89 | resultsCacheTtl: Duration.minutes(0), 90 | identitySource: IdentitySource.header('Authorization'), 91 | authorizerName: `${id}Authorizer`, 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/integration/lambda.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Grant } from 'aws-cdk-lib/aws-iam'; 3 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 4 | import { aws_lambda_nodejs as lambdaNodejs, Duration } from 'aws-cdk-lib'; 5 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 6 | import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway'; 7 | 8 | export class LambdaFunction { 9 | private function: NodejsFunction; 10 | constructor(scope: Construct, id: string, entry: string, environ?: { [key: string]: string }) { 11 | const lambdaFunctionEnviron = environ ? environ : {}; 12 | this.function = new lambdaNodejs.NodejsFunction(scope, id, { 13 | runtime: Runtime.NODEJS_18_X, 14 | entry: entry, 15 | handler: 'handler', 16 | bundling: { 17 | minify: true, 18 | sourceMap: true, 19 | target: 'es2018', 20 | }, 21 | timeout: Duration.seconds(3), 22 | }); 23 | Object.keys(lambdaFunctionEnviron).forEach((key) => { 24 | this.function.addEnvironment(key, lambdaFunctionEnviron[key]); 25 | }); 26 | } 27 | 28 | get Function(): NodejsFunction { 29 | return this.function; 30 | } 31 | 32 | get Integration(): LambdaIntegration { 33 | return new LambdaIntegration(this.function); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { LambdaFunction } from '../integration/lambda'; 3 | import * as path from 'path'; 4 | import { Resource, IResource, TokenAuthorizer, Method } from 'aws-cdk-lib/aws-apigateway'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import * as fs from 'fs'; 7 | import { IndexRoutes } from './routes'; 8 | 9 | import { IRoutes } from './routes/interface'; 10 | import { FileRoutes } from './routes/file'; 11 | import { FileIdRoutes } from './routes/fileId'; 12 | 13 | export interface RoutesProps { 14 | authorizer: TokenAuthorizer; 15 | originDomain: string; 16 | assetsBucket: Bucket; 17 | } 18 | 19 | export class APIRoutes { 20 | readonly resource: Resource | IResource; 21 | readonly authorizer: TokenAuthorizer; 22 | readonly originDomain: string; 23 | readonly scope: Construct; 24 | readonly id: string; 25 | readonly assetsBucket: Bucket; 26 | private readonly routes: { [key: string]: { [key: string]: Method } }; 27 | constructor(scope: Construct, id: string, resource: Resource | IResource, props: RoutesProps) { 28 | this.id = id; 29 | this.scope = scope; 30 | this.originDomain = props.originDomain; 31 | this.resource = resource; 32 | this.authorizer = props.authorizer; 33 | this.assetsBucket = props.assetsBucket; 34 | this.routes = this.createAPIEndpoints(); 35 | } 36 | get Routes() { 37 | return this.routes; 38 | } 39 | private createAPIEndpoints(): { [key: string]: { [key: string]: Method } } { 40 | const fileResource = this.resource.addResource('api').addResource('file'); 41 | const fileIdResource = fileResource.addResource('{fileId}'); 42 | 43 | const indexRoutes = new IndexRoutes(this.scope, this.id, this.resource, this.originDomain); 44 | const fileRoutes = new FileRoutes(this.scope, this.id, fileResource, this.originDomain, this.authorizer, this.assetsBucket); 45 | const fileIdRoutes = new FileIdRoutes(this.scope, this.id, fileIdResource, this.originDomain, this.authorizer, this.assetsBucket); 46 | 47 | const indexMethod = { 48 | GET: indexRoutes.GET, 49 | }; 50 | 51 | const fileMethod = { 52 | GET: fileRoutes.GET, 53 | POST: fileRoutes.POST, 54 | }; 55 | 56 | const fileIdMethod = { 57 | GET: fileIdRoutes.GET, 58 | }; 59 | 60 | return { 61 | '/': indexMethod, 62 | '/api/file': fileMethod, 63 | '/api/file/{fileId}': fileIdMethod, 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import { IRoutes } from './interface'; 2 | import { Construct } from 'constructs'; 3 | import { Resource, IResource, Method } from 'aws-cdk-lib/aws-apigateway'; 4 | 5 | export class BaseRoutes implements IRoutes { 6 | readonly resource: Resource | IResource; 7 | readonly origin: string; 8 | readonly scope: Construct; 9 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 10 | this.scope = scope; 11 | this.origin = origin; 12 | this.resource = resource; 13 | } 14 | 15 | get GET(): Method | undefined { 16 | return undefined; 17 | } 18 | get POST(): Method | undefined { 19 | return undefined; 20 | } 21 | get PUT(): Method | undefined { 22 | return undefined; 23 | } 24 | get DELETE(): Method | undefined { 25 | return undefined; 26 | } 27 | get PATCH(): Method | undefined { 28 | return undefined; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/file.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource, TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import { BaseRoutes } from './base'; 5 | import * as path from 'path'; 6 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 7 | 8 | export class FileRoutes extends BaseRoutes { 9 | private authorizer: TokenAuthorizer; 10 | private bucket: Bucket; 11 | 12 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string, authorizer: TokenAuthorizer, bucket: Bucket) { 13 | super(scope, id, resource, origin); 14 | this.authorizer = authorizer; 15 | this.bucket = bucket; 16 | } 17 | 18 | private get lambdaEnvironParams() { 19 | return { 20 | ORIGIN_DOMAIN: this.origin, 21 | BUCKET_NAME: this.bucket.bucketName, 22 | }; 23 | } 24 | 25 | override get GET() { 26 | const lambdaFunction = new LambdaFunction(this.resource, 'ListFile', path.join(__dirname, 'functions', 'listFile.ts'), this.lambdaEnvironParams); 27 | this.bucket.grantRead(lambdaFunction.Function); 28 | return this.resource.addMethod('GET', lambdaFunction.Integration, { 29 | apiKeyRequired: false, 30 | methodResponses: [ 31 | { 32 | statusCode: '200', 33 | }, 34 | ], 35 | authorizer: this.authorizer, 36 | }); 37 | } 38 | 39 | override get POST() { 40 | const lambdaFunction = new LambdaFunction(this.resource, 'PostFile', path.join(__dirname, 'functions', 'postFile.ts'), this.lambdaEnvironParams); 41 | this.bucket.grantWrite(lambdaFunction.Function); 42 | return this.resource.addMethod('POST', lambdaFunction.Integration, { 43 | apiKeyRequired: false, 44 | methodResponses: [ 45 | { 46 | statusCode: '200', 47 | }, 48 | ], 49 | authorizer: this.authorizer, 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/fileId.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource, TokenAuthorizer } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import { BaseRoutes } from './base'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import * as path from 'path'; 7 | 8 | export class FileIdRoutes extends BaseRoutes { 9 | private authorizer: TokenAuthorizer; 10 | private bucket: Bucket; 11 | 12 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string, authorizer: TokenAuthorizer, bucket: Bucket) { 13 | super(scope, id, resource, origin); 14 | this.authorizer = authorizer; 15 | this.bucket = bucket; 16 | } 17 | private get lambdaEnvironParams() { 18 | return { 19 | ORIGIN_DOMAIN: this.origin, 20 | BUCKET_NAME: this.bucket.bucketName, 21 | }; 22 | } 23 | override get GET() { 24 | const lambdaFunction = new LambdaFunction(this.resource, 'GetFileSign', path.join(__dirname, 'functions', 'getFileSign.ts'), this.lambdaEnvironParams); 25 | this.bucket.grantRead(lambdaFunction.Function); 26 | return this.resource.addMethod('GET', lambdaFunction.Integration, { 27 | apiKeyRequired: false, 28 | methodResponses: [ 29 | { 30 | statusCode: '200', 31 | }, 32 | ], 33 | authorizer: this.authorizer, 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/functions/const.ts: -------------------------------------------------------------------------------- 1 | export const RESPONSE_BASE_HEADERS = { 2 | 'Content-Type': 'application/json', 3 | 'Access-Control-Allow-Methods': 'GET,OPTIONS', 4 | 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token', 5 | }; 6 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/functions/getFileSign.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; 3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 4 | import { RESPONSE_BASE_HEADERS } from './const'; 5 | import * as path from 'path'; 6 | 7 | const s3 = new S3Client({ region: 'ap-northeast-1' }); 8 | 9 | const RESPONSE_HEADERS = { 10 | ...RESPONSE_BASE_HEADERS, 11 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 12 | }; 13 | 14 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 15 | return { 16 | statusCode, 17 | headers: RESPONSE_HEADERS, 18 | body, 19 | }; 20 | }; 21 | 22 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 23 | const { id } = event.requestContext.authorizer as { id: string }; 24 | const { fileId } = event.pathParameters as { fileId: string }; 25 | 26 | const decodetFileId = decodeURIComponent(fileId); 27 | if (decodetFileId.includes('/')) return response(400, JSON.stringify({ message: 'Invalid fileId' })); 28 | const key = path.normalize(`${id}/${decodetFileId}`); 29 | 30 | const command = new GetObjectCommand({ 31 | Bucket: process.env.BUCKET_NAME as string, 32 | Key: key, 33 | }); 34 | 35 | const url = await getSignedUrl(s3, command, { expiresIn: 60 }); 36 | 37 | return response(200, JSON.stringify({ url })); 38 | }; 39 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { RESPONSE_BASE_HEADERS } from './const'; 3 | 4 | const RESPONSE_HEADERS = { 5 | ...RESPONSE_BASE_HEADERS, 6 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 7 | }; 8 | 9 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 10 | return { 11 | statusCode, 12 | headers: RESPONSE_HEADERS, 13 | body, 14 | }; 15 | }; 16 | 17 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 18 | return response(200, JSON.stringify({ message: 'Hello, world!' })); 19 | }; 20 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/functions/listFile.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { ListObjectsV2Command, ListObjectsV2CommandInput, S3Client } from '@aws-sdk/client-s3'; 3 | import { RESPONSE_BASE_HEADERS } from './const'; 4 | 5 | const s3 = new S3Client({ region: 'ap-northeast-1' }); 6 | 7 | const RESPONSE_HEADERS = { 8 | ...RESPONSE_BASE_HEADERS, 9 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 10 | }; 11 | 12 | const getListObjects = async ( 13 | id: string, 14 | nextToken?: string, 15 | ): Promise<{ 16 | Files?: (string | undefined)[]; 17 | NextToken?: string; 18 | }> => { 19 | const listObjectsV2CommandInput: ListObjectsV2CommandInput = nextToken 20 | ? { 21 | Bucket: process.env.BUCKET_NAME as string, 22 | MaxKeys: 10, 23 | ContinuationToken: nextToken, 24 | } 25 | : { 26 | Bucket: process.env.BUCKET_NAME as string, 27 | MaxKeys: 10, 28 | }; 29 | if (id !== '') listObjectsV2CommandInput.Prefix = `${id}/`; 30 | const listObjectsV2Command = new ListObjectsV2Command(listObjectsV2CommandInput); 31 | const listObjectsV2CommandResult = await s3.send(listObjectsV2Command); 32 | 33 | if (!listObjectsV2CommandResult.Contents) return { Files: [] }; 34 | 35 | const files = listObjectsV2CommandResult.Contents.sort((a, b) => { 36 | if (a.LastModified && b.LastModified) { 37 | if (a.LastModified < b.LastModified) return 1; 38 | if (a.LastModified > b.LastModified) return -1; 39 | return 0; 40 | } 41 | return 0; 42 | }) 43 | .filter((content) => { 44 | return content.Key?.split('/').length === 1 || (content.Key?.split('/').length === 2 && content.Key?.split('/')[1] !== ''); 45 | }) 46 | .map((content) => { 47 | return content.Key?.split('/')[1]; 48 | }); 49 | 50 | return { 51 | Files: files, 52 | NextToken: listObjectsV2CommandResult.NextContinuationToken, 53 | }; 54 | }; 55 | 56 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 57 | return { 58 | statusCode, 59 | headers: RESPONSE_HEADERS, 60 | body, 61 | }; 62 | }; 63 | 64 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 65 | const { id } = event.requestContext.authorizer as { id: string }; 66 | const { nextToken } = event.queryStringParameters || {}; 67 | 68 | const { Files, NextToken } = await getListObjects(id, nextToken); 69 | return response( 70 | 200, 71 | JSON.stringify({ 72 | files: Files, 73 | nextToken: NextToken ? NextToken : '', 74 | }), 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/functions/postFile.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 4 | import { RESPONSE_BASE_HEADERS } from './const'; 5 | 6 | const s3 = new S3Client({ region: 'ap-northeast-1' }); 7 | 8 | const isAllowedFileSize = (size: number): boolean => { 9 | return 0 <= size && size <= 10 * 1024 * 1024; 10 | }; 11 | 12 | const isAllowedFileName = (fileId: string): boolean => { 13 | return fileId !== undefined && fileId !== '' && !decodeURIComponent(fileId).includes('/'); 14 | }; 15 | 16 | const RESPONSE_HEADERS = { 17 | ...RESPONSE_BASE_HEADERS, 18 | 'Access-Control-Allow-Origin': `${process.env.ORIGIN_DOMAIN as string}`, 19 | 'Access-Control-Allow-Methods': 'POST,OPTIONS', 20 | }; 21 | 22 | const response = (statusCode: number, body: string): APIGatewayProxyResult => { 23 | return { 24 | statusCode, 25 | headers: RESPONSE_HEADERS, 26 | body, 27 | }; 28 | }; 29 | 30 | export const handler = async (event: APIGatewayProxyEvent): Promise => { 31 | const { id } = event.requestContext.authorizer as { id: string }; 32 | const { fileId, size } = JSON.parse(event.body as string) as { fileId: string; size: number }; 33 | 34 | if (!isAllowedFileSize(size)) return response(400, JSON.stringify({ message: 'File size is too large' })); 35 | if (!isAllowedFileName(fileId)) return response(400, JSON.stringify({ message: 'Invalid file name' })); 36 | 37 | const command = new PutObjectCommand({ 38 | Bucket: process.env.BUCKET_NAME as string, 39 | Key: `${id}/${decodeURIComponent(fileId)}`, 40 | ContentType: 'application/octet-stream', 41 | ContentLength: size, 42 | }); 43 | const url = await getSignedUrl(s3, command, { 44 | expiresIn: 60, 45 | }); 46 | 47 | return response(200, JSON.stringify({ url })); 48 | }; 49 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Resource, IResource } from 'aws-cdk-lib/aws-apigateway'; 2 | import { LambdaFunction } from '../../integration/lambda'; 3 | import { Construct } from 'constructs'; 4 | import * as path from 'path'; 5 | import { BaseRoutes } from './base'; 6 | export class IndexRoutes extends BaseRoutes { 7 | constructor(scope: Construct, id: string, resource: Resource | IResource, origin: string) { 8 | super(scope, id, resource, origin); 9 | } 10 | override get GET() { 11 | const env = { 12 | ORIGIN_DOMAIN: this.origin, 13 | }; 14 | const integration = new LambdaFunction(this.resource, 'Index', path.join(__dirname, 'functions', 'index.ts'), env).Integration; 15 | return this.resource.addMethod('GET', integration, { 16 | apiKeyRequired: false, 17 | methodResponses: [ 18 | { 19 | statusCode: '200', 20 | }, 21 | ], 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/api/router/routes/interface.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'aws-cdk-lib/aws-apigateway'; 2 | 3 | export interface IRoutes { 4 | GET?: Method; 5 | POST?: Method; 6 | PUT?: Method; 7 | DELETE?: Method; 8 | PATCH?: Method; 9 | HEAD?: Method; 10 | } 11 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/const.ts: -------------------------------------------------------------------------------- 1 | export const CHALLENGE_NAME = `Challenge4`; 2 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, Fn } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { CHALLENGE_NAME } from './const'; 4 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 5 | import { WebSiteDeployment } from './website/deployment'; 6 | 7 | /** 8 | * DeploymentStack 9 | * @class 10 | * @extends Stack 11 | * 12 | * このStackは、Challenge4のためのフロントエンドのデプロイを行います。 13 | * 主にこのスタックで構築されるリソースは以下の通りです。 14 | * - Amazon S3 Bucket 15 | */ 16 | export class DeploymentStack extends Stack { 17 | constructor(scope: Construct, props?: StackProps) { 18 | super(scope, `${CHALLENGE_NAME}DeploymentStack`, props); 19 | const staticBucketName = Fn.importValue(`${CHALLENGE_NAME}WebSiteBucketName`); 20 | const staticBucket = Bucket.fromBucketName(this, `${CHALLENGE_NAME}StaticBucket`, staticBucketName); 21 | new WebSiteDeployment(this, `${CHALLENGE_NAME}WebSiteDeployment`, staticBucket); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/idp/userPool/functions/emailValidate.ts: -------------------------------------------------------------------------------- 1 | import { PreSignUpEmailTriggerEvent } from 'aws-lambda'; 2 | import { invalidEmailDomains } from './const'; 3 | /** 4 | * PreSignUpEmailTrigger 5 | * @param {PreSignUpEmailTriggerEvent} event 6 | * @returns {PreSignUpEmailTriggerEvent} 7 | * 8 | * この関数は、CognitoのPreSignUpEmailTriggerに紐づけられています。 9 | * この関数は、ユーザーがサインアップする際に実行されます。 10 | * 関数ないでは、EmailのAliasが含まれていないことや有名な各種捨てメアドのドメインでないことを確認し、EDoSにつながる可能性のあるユーザーのサインアップを防ぎます。 11 | */ 12 | export const handler = async (event: PreSignUpEmailTriggerEvent) => { 13 | const email = event.request.userAttributes.email; 14 | if (email === undefined) { 15 | throw new Error('Email is not defined'); 16 | } 17 | const emailAlias = email.split('+')[1]; 18 | if (emailAlias !== undefined) { 19 | throw new Error('Email Alias is not allowed'); 20 | } 21 | const emailDomain = email.split('@')[1]; 22 | if (invalidEmailDomains.includes(emailDomain)) { 23 | throw new Error('Email Domain is not allowed'); 24 | } 25 | return event; 26 | }; 27 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/idp/userPool/functions/user.ts: -------------------------------------------------------------------------------- 1 | import { Callback, Context, PostConfirmationTriggerEvent, PostConfirmationTriggerHandler } from 'aws-lambda'; 2 | import { CognitoIdentityProviderClient, AdminUpdateUserAttributesCommand, AdminUpdateUserAttributesCommandInput } from '@aws-sdk/client-cognito-identity-provider'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | const cognito = new CognitoIdentityProviderClient({ region: 'ap-northeast-1' }); 5 | 6 | /** 7 | * PostConfirmationTrigger 8 | * @param {PostConfirmationTriggerEvent} event 9 | * @param {Context} context 10 | * @param {Callback} callback 11 | * 12 | * この関数は、CognitoのPostConfirmationTriggerに紐づけられています。 13 | * この関数は、ユーザーがサインアップした後に実行されます。 14 | * 関数内では、ユーザーの属性にアプリケーション独自のUserのIdを追加します 15 | */ 16 | export const handler: PostConfirmationTriggerHandler = (event: PostConfirmationTriggerEvent, context: Context, callback: Callback) => { 17 | console.log(event); 18 | const userId = uuidv4(); 19 | const updateUserAttributesCommandParam: AdminUpdateUserAttributesCommandInput = { 20 | UserPoolId: event.userPoolId, 21 | Username: event.userName, 22 | UserAttributes: [ 23 | { 24 | Name: 'custom:id', 25 | Value: userId, 26 | }, 27 | ], 28 | }; 29 | const updateUserAttributesCommand = new AdminUpdateUserAttributesCommand(updateUserAttributesCommandParam); 30 | cognito.send(updateUserAttributesCommand).then((res) => {}); 31 | callback(null, event); 32 | }; 33 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/index.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { IdPool } from './idp/userPool'; 4 | import { WebSite } from './website'; 5 | import { CHALLENGE_NAME } from './const'; 6 | import { RestApiGateway } from './api'; 7 | 8 | /** 9 | * ApplicationStack 10 | * @class 11 | * @extends Stack 12 | * 13 | * このStackは、Challenge4のためのリソースを構築します。 14 | * 主にこのスタックで構築されるリソースは以下の通りです。 15 | * - Amazon S3 Bucket 16 | * - Amazon Cognito User Pool 17 | * - Amazon CloudFront 18 | * - Amazon API Gateway 19 | * - AWS Lambda 20 | */ 21 | export class ApplicationStack extends Stack { 22 | readonly WebSite: WebSite; 23 | readonly IdPool: IdPool; 24 | readonly API: RestApiGateway; 25 | readonly id: string = CHALLENGE_NAME; 26 | constructor(scope: Construct, uniqueId: string, props?: StackProps) { 27 | super(scope, `${CHALLENGE_NAME}ApplicationStack`, props); 28 | this.WebSite = new WebSite(this, this.id, uniqueId); 29 | this.IdPool = new IdPool(this, this.id, { 30 | redirectDomain: this.WebSite.CloudFront.distributionDomainName, 31 | }); 32 | this.API = new RestApiGateway(this, this.id, { 33 | origin: `https://${this.WebSite.CloudFront.distributionDomainName}`, 34 | userPoolId: this.IdPool.userPoolId, 35 | userPoolClientId: this.IdPool.client.userPoolClientId, 36 | assetsBucket: this.WebSite.AssetsS3Bucket, 37 | }); 38 | this.output(); 39 | } 40 | 41 | output() { 42 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteURL`, { 43 | value: `https://${this.WebSite.CloudFront.distributionDomainName}`, 44 | exportName: `${CHALLENGE_NAME}WebSiteURL`, 45 | }); 46 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteOrigin`, { 47 | value: this.WebSite.CloudFront.distributionDomainName, 48 | exportName: `${CHALLENGE_NAME}WebSiteOrigin`, 49 | }); 50 | new CfnOutput(this, `${CHALLENGE_NAME}WebSiteBucketName`, { 51 | value: this.WebSite.StaticWebS3Bucket.bucketName, 52 | exportName: `${CHALLENGE_NAME}WebSiteBucketName`, 53 | }); 54 | new CfnOutput(this, `${CHALLENGE_NAME}AssetsBucketName`, { 55 | value: this.WebSite.AssetsS3Bucket.bucketName, 56 | exportName: `${CHALLENGE_NAME}AssetsBucketName`, 57 | }); 58 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolId`, { 59 | value: this.IdPool.userPoolId, 60 | exportName: `${CHALLENGE_NAME}UserPoolId`, 61 | }); 62 | new CfnOutput(this, `${CHALLENGE_NAME}UserPoolClientId`, { 63 | value: this.IdPool.client.userPoolClientId, 64 | exportName: `${CHALLENGE_NAME}UserPoolClientId`, 65 | }); 66 | new CfnOutput(this, `${CHALLENGE_NAME}APIGatewayURL`, { 67 | value: this.API.url, 68 | exportName: `${CHALLENGE_NAME}APIGatewayURL`, 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/website/assets/flag: -------------------------------------------------------------------------------- 1 | CDK{Challenge4} -------------------------------------------------------------------------------- /cdk/lib/challenge4/website/assetsBucket.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy } from 'aws-cdk-lib'; 2 | import { Bucket, BucketProps, BucketEncryption, HttpMethods } from 'aws-cdk-lib/aws-s3'; 3 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 4 | import { Construct } from 'constructs'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | 8 | export interface AssetsBucketProps extends BucketProps { 9 | origin: string; 10 | } 11 | 12 | /** 13 | * AssetsBucket 14 | * @class 15 | * @extends Bucket 16 | * 17 | * このクラスは、アップロードされたファイルを保存するためのS3バケットを構築します。 18 | */ 19 | export class AssetsBucket extends Bucket { 20 | constructor(scope: Construct, id: string, props: AssetsBucketProps) { 21 | super(scope, id, { 22 | ...props, 23 | bucketName: `${id.toLowerCase()}-assets-bucket`, 24 | encryption: BucketEncryption.S3_MANAGED, 25 | removalPolicy: RemovalPolicy.DESTROY, 26 | autoDeleteObjects: true, 27 | lifecycleRules: [], 28 | cors: [ 29 | { 30 | allowedMethods: [HttpMethods.GET, HttpMethods.HEAD, HttpMethods.PUT], 31 | allowedOrigins: [`https://${props.origin}`], 32 | allowedHeaders: ['*'], 33 | }, 34 | ], 35 | publicReadAccess: false, 36 | blockPublicAccess: { 37 | blockPublicAcls: true, 38 | blockPublicPolicy: false, 39 | ignorePublicAcls: true, 40 | restrictPublicBuckets: false, 41 | }, 42 | }); 43 | if (!props) return; 44 | this.defaultAssetsDeploy(scope); 45 | } 46 | 47 | private defaultAssetsDeploy(scope: Construct) { 48 | const flag = fs.readFileSync(path.join(__dirname, 'assets', 'flag'), 'utf-8'); 49 | const sources = [Source.data('flag.txt', flag)]; 50 | new BucketDeployment(scope, 'AssetsBucketDeployment', { 51 | destinationBucket: this, 52 | sources: sources, 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cdk/lib/challenge4/website/deployment.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 3 | import * as path from 'path'; 4 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 5 | import { CHALLENGE_NAME } from '../const'; 6 | 7 | export class WebSiteDeployment extends BucketDeployment { 8 | constructor(scope: Construct, id: string, destinationBucket: IBucket) { 9 | const assetsPath = path.join('/app', 'frontend', CHALLENGE_NAME.toLowerCase(), 'build'); 10 | console.log(assetsPath); 11 | super(scope, `${id}WebSiteDeployment`, { 12 | destinationBucket, 13 | sources: [ 14 | Source.data('error.html', '

Error

Not Found

./${CHALLENGE_NAME}/src/env.json 5 | cd $DIR 6 | echo "Building $DIR" 7 | npm install 8 | npm run build 9 | cd .. 10 | done -------------------------------------------------------------------------------- /frontend/challenge1/Docs.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge1/Docs.md -------------------------------------------------------------------------------- /frontend/challenge1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "challenge1", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^4.6.1", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.26", 12 | "@types/react": "^18.2.6", 13 | "@types/react-dom": "^18.2.4", 14 | "aws-amplify": "^5.2.0", 15 | "axios": "^1.4.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.11.1", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/challenge1/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge1/public/favicon.ico -------------------------------------------------------------------------------- /frontend/challenge1/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Challenge 1 - privilege escalation 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/challenge1/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge1/public/logo192.png -------------------------------------------------------------------------------- /frontend/challenge1/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge1/public/logo512.png -------------------------------------------------------------------------------- /frontend/challenge1/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/challenge1/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/challenge1/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/challenge1/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/challenge1/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import { Amplify, Auth } from 'aws-amplify'; 5 | import { Authenticator, Grid, Card, Button, Heading, useTheme } from '@aws-amplify/ui-react'; 6 | import '@aws-amplify/ui-react/styles.css'; 7 | import env from './env.json'; 8 | 9 | Amplify.configure({ 10 | aws_project_region: 'ap-northeast-1', 11 | aws_cognito_region: 'ap-northeast-1', 12 | aws_user_pools_id: env.Challenge1UserPoolId, 13 | aws_user_pools_web_client_id: env.Challenge1UserPoolClientId, 14 | authenticationFlowType: 'USER_PASSWORD_AUTH', 15 | }); 16 | 17 | async function getMessage() { 18 | try { 19 | const token = (await Auth.currentSession()).getIdToken().getJwtToken(); 20 | const response = await fetch(`${env.Challenge1APIGatewayURL}/api/flag`, { 21 | headers: { 22 | Authorization: `${token}`, 23 | }, 24 | }); 25 | const { message } = await response.json(); 26 | 27 | return `${message}`; 28 | } catch (error) { 29 | return 'Hello World!'; 30 | } 31 | } 32 | 33 | function AuthenticatedPage() { 34 | const [message, setMessage] = React.useState('Hello World'); 35 | React.useEffect(() => { 36 | (async () => { 37 | setMessage(await getMessage()); 38 | })(); 39 | }, []); 40 | return ( 41 |
42 |

{message}

43 | 51 |
52 | ); 53 | } 54 | 55 | function App() { 56 | return ( 57 |
58 | 63 | 67 | 77 | Challenge1 Sign in 78 | 79 | ); 80 | }, 81 | Footer() { 82 | return <>; 83 | }, 84 | }, 85 | }} 86 | > 87 | 88 | 89 | 90 | 91 |
92 | ); 93 | } 94 | 95 | export default App; 96 | -------------------------------------------------------------------------------- /frontend/challenge1/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/challenge1/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /frontend/challenge1/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/challenge1/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/challenge1/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/challenge1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/challenge2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "challenge1", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^4.6.1", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.26", 12 | "@types/react": "^18.2.6", 13 | "@types/react-dom": "^18.2.4", 14 | "aws-amplify": "^5.2.0", 15 | "axios": "^1.4.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.11.1", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/challenge2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge2/public/favicon.ico -------------------------------------------------------------------------------- /frontend/challenge2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Challenge 2 - selfie 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/challenge2/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge2/public/logo192.png -------------------------------------------------------------------------------- /frontend/challenge2/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge2/public/logo512.png -------------------------------------------------------------------------------- /frontend/challenge2/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/challenge2/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/challenge2/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/challenge2/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/challenge2/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import { Amplify, Auth } from 'aws-amplify'; 5 | import { Authenticator, Grid, Card, Button, Heading, useTheme } from '@aws-amplify/ui-react'; 6 | import '@aws-amplify/ui-react/styles.css'; 7 | import env from './env.json'; 8 | 9 | Amplify.configure({ 10 | aws_project_region: 'ap-northeast-1', 11 | aws_cognito_region: 'ap-northeast-1', 12 | aws_user_pools_id: env.Challenge2UserPoolId, 13 | aws_user_pools_web_client_id: env.Challenge2UserPoolClientId, 14 | authenticationFlowType: 'USER_PASSWORD_AUTH', 15 | }); 16 | 17 | async function getMessage() { 18 | try { 19 | const token = (await Auth.currentSession()).getIdToken().getJwtToken(); 20 | const response = await fetch(`${env.Challenge2APIGatewayURL}/api/flag`, { 21 | headers: { 22 | Authorization: `${token}`, 23 | }, 24 | }); 25 | const { message } = await response.json(); 26 | 27 | return `${message}`; 28 | } catch (error) { 29 | return 'Hello World!'; 30 | } 31 | } 32 | 33 | function AuthenticatedPage() { 34 | const [message, setMessage] = React.useState('Hello World'); 35 | React.useEffect(() => { 36 | (async () => { 37 | setMessage(await getMessage()); 38 | })(); 39 | }, []); 40 | return ( 41 |
42 |

{message}

43 | 51 |
52 | ); 53 | } 54 | 55 | function App() { 56 | return ( 57 |
58 | 63 | 67 | 78 | Challenge2 Sign in 79 | 80 | ); 81 | }, 82 | }, 83 | }} 84 | > 85 | 86 | 87 | 88 | 89 |
90 | ); 91 | } 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /frontend/challenge2/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/challenge2/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /frontend/challenge2/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/challenge2/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/challenge2/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/challenge2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/challenge3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "challenge2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^4.6.1", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.26", 12 | "@types/react": "^18.2.6", 13 | "@types/react-dom": "^18.2.4", 14 | "aws-amplify": "^5.2.0", 15 | "axios": "^1.4.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.11.1", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/challenge3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge3/public/favicon.ico -------------------------------------------------------------------------------- /frontend/challenge3/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Challenge 3 - File Box 1 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/challenge3/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge3/public/logo192.png -------------------------------------------------------------------------------- /frontend/challenge3/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge3/public/logo512.png -------------------------------------------------------------------------------- /frontend/challenge3/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/challenge3/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/challenge3/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/challenge3/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/challenge3/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { Auth } from 'aws-amplify'; 4 | import { Authenticator, Grid, Card, Button, Heading, useTheme } from '@aws-amplify/ui-react'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import AuthenticatedPage from './AuthenticatedPage'; 7 | 8 | 9 | 10 | function App() { 11 | const [isAuth, setAuth] = React.useState(false); 12 | React.useEffect(() => { 13 | (async () => { 14 | try { 15 | await Auth.currentSession(); 16 | setAuth(true); 17 | } catch (error) { 18 | setAuth(false); 19 | } 20 | })(); 21 | }, []); 22 | return ( 23 |
24 | { 25 | !isAuth ? ( 26 | 30 | 34 | Challenge3 Sign in 44 | ); 45 | }, 46 | } 47 | }} 48 | > 49 | { 50 | () => { 51 | setAuth(true); 52 | return ( 53 | <> 54 | ); 55 | } 56 | } 57 | 58 | 59 | 60 | ) : ( 61 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | ) 78 | } 79 |
80 | ); 81 | } 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /frontend/challenge3/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/challenge3/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { Amplify } from 'aws-amplify'; 7 | import env from './env.json'; 8 | Amplify.configure({ 9 | aws_project_region: 'ap-northeast-1', 10 | aws_cognito_region: 'ap-northeast-1', 11 | aws_user_pools_id: env.Challenge3UserPoolId, 12 | aws_user_pools_web_client_id: env.Challenge3UserPoolClientId, 13 | authenticationFlowType: 'USER_PASSWORD_AUTH', 14 | }); 15 | 16 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 17 | root.render( 18 | 19 | 20 | , 21 | ); 22 | 23 | // If you want to start measuring performance in your app, pass a function 24 | // to log results (for example: reportWebVitals(console.log)) 25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 26 | reportWebVitals(); 27 | -------------------------------------------------------------------------------- /frontend/challenge3/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/challenge3/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/challenge3/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/challenge3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/challenge4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "challenge2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-amplify/ui-react": "^4.6.1", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.18.26", 12 | "@types/react": "^18.2.6", 13 | "@types/react-dom": "^18.2.4", 14 | "aws-amplify": "^5.2.0", 15 | "axios": "^1.4.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.11.1", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.5", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/challenge4/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge4/public/favicon.ico -------------------------------------------------------------------------------- /frontend/challenge4/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Challenge 4 - File Box 2 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/challenge4/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge4/public/logo192.png -------------------------------------------------------------------------------- /frontend/challenge4/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/challenge4/public/logo512.png -------------------------------------------------------------------------------- /frontend/challenge4/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/challenge4/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/challenge4/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/challenge4/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/challenge4/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { Auth } from 'aws-amplify'; 4 | import { Authenticator, Grid, Card, Button, Heading, useTheme } from '@aws-amplify/ui-react'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import AuthenticatedPage from './AuthenticatedPage'; 7 | 8 | 9 | 10 | function App() { 11 | const [isAuth, setAuth] = React.useState(false); 12 | React.useEffect(() => { 13 | (async () => { 14 | try { 15 | await Auth.currentSession(); 16 | setAuth(true); 17 | } catch (error) { 18 | setAuth(false); 19 | } 20 | })(); 21 | }, []); 22 | return ( 23 |
24 | { 25 | !isAuth ? ( 26 | 30 | 34 | Challenge4 Sign in 44 | ); 45 | }, 46 | } 47 | }} 48 | > 49 | { 50 | () => { 51 | setAuth(true); 52 | return ( 53 | <> 54 | ); 55 | } 56 | } 57 | 58 | 59 | 60 | ) : ( 61 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | ) 78 | } 79 |
80 | ); 81 | } 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /frontend/challenge4/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/challenge4/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { Amplify } from 'aws-amplify'; 7 | import env from './env.json'; 8 | 9 | Amplify.configure({ 10 | aws_project_region: 'ap-northeast-1', 11 | aws_cognito_region: 'ap-northeast-1', 12 | aws_user_pools_id: env.Challenge4UserPoolId, 13 | aws_user_pools_web_client_id: env.Challenge4UserPoolClientId, 14 | authenticationFlowType: 'USER_PASSWORD_AUTH', 15 | }); 16 | 17 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); 18 | root.render( 19 | 20 | 21 | , 22 | ); 23 | 24 | // If you want to start measuring performance in your app, pass a function 25 | // to log results (for example: reportWebVitals(console.log)) 26 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 27 | reportWebVitals(); 28 | -------------------------------------------------------------------------------- /frontend/challenge4/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/challenge4/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/challenge4/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/challenge4/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/react-build.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-zara-n/aws-cdk-security-challenges/8de20102adc851fbf36cc0fa5b1e5c055d9db264/frontend/react-build.sh -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | init: 2 | cd ./deploy && \ 3 | docker compose build --no-cache && \ 4 | docker compose run --rm installer bash -c "bash /app/init.sh" 5 | 6 | synth: 7 | cd ./deploy && \ 8 | docker compose run --rm cdk bash -c "cdk synth --all" 9 | 10 | setup: 11 | cd ./deploy && \ 12 | docker compose run --rm cdk bash -c "cdk deploy -O ../frontend/aws.json Challenge1ApplicationStack Challenge2ApplicationStack Challenge3ApplicationStack Challenge4ApplicationStack" && \ 13 | docker compose run --rm react bash -c "bash ./react-build.sh" && \ 14 | docker compose run --rm cdk bash -c "cdk deploy Challenge1DeploymentStack Challenge2DeploymentStack Challenge3DeploymentStack Challenge4DeploymentStack" 15 | 16 | destroy: 17 | cd ./deploy && \ 18 | docker compose run --rm cdk bash -c "cdk destroy -f" 19 | 20 | bash-cdk: 21 | cd ./deploy && \ 22 | docker compose run --rm cdk bash 23 | 24 | bash-react: 25 | cd ./deploy && \ 26 | docker compose run --rm react bash --------------------------------------------------------------------------------