diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/.eslintrc.js b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/.gitignore b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.gitignore new file mode 100644 index 0000000000000..becda34c45624 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.gitignore @@ -0,0 +1,17 @@ +*.d.ts +*.generated.ts +*.js +*.js.map +*.snk +.jsii +.LAST_BUILD +.LAST_PACKAGE +nyc.config.js +.nyc_output +coverage +dist +tsconfig.json +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/.npmignore b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.npmignore new file mode 100644 index 0000000000000..093c734b1bd2f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.npmignore @@ -0,0 +1,28 @@ +# The basics +*.ts +*.tgz +*.snk +!*.d.ts +!*.js +**/cdk.out + +# Coverage +coverage +.nyc_output +.nycrc + +# Build gear +dist +.LAST_BUILD +.LAST_PACKAGE + +*.tsbuildinfo +tsconfig.json +!.jsii +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/LICENSE b/packages/@aws-cdk/aws-apigatewayv2-authorizers/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/NOTICE b/packages/@aws-cdk/aws-apigatewayv2-authorizers/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md b/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md new file mode 100644 index 0000000000000..9591ecfe4ffde --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md @@ -0,0 +1,82 @@ +# AWS APIGatewayv2 Authorizers + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + +## Table of Contents + +- [HTTP APIs](#http-apis) +- [JWT Authorizers](#jwt-authorizers) + - [User Pool Authorizer](#user-pool-authorizer) + +## HTTP APIs + +API Gateway supports multiple mechanisms for controlling and managing access to your HTTP API. They are mainly +classified into Lambda Authorizers, JWT authorizers and standard AWS IAM roles and policies. More information is +available at [Controlling and managing access to an HTTP +API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html). + +## JWT Authorizers + +JWT authorizers allow the use of JSON Web Tokens (JWTs) as part of [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) and [OAuth 2.0](https://oauth.net/2/) frameworks to allow and restrict clients from accessing HTTP APIs. + +When configured on a route, the API Gateway service validates the JWTs submitted by the client, and allows or denies access based on its content. + +API gateway uses the `identitySource` to determine where to look for the token. By default it checks the http `Authorization` header. However it also [supports a number of other options](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.identity-sources). It then decodes the JWT and validates the signature and claims, against the options defined in the authorizer and route (scopes). For more information check the [JWT Authorizer documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). + +```ts +const authorizer = new HttpJwtAuthorizer({ + jwtAudience: ['3131231'], + jwtIssuer: 'https://test.us.auth0.com', +}); + +const api = new HttpApi(stack, 'HttpApi'); + +api.addRoutes({ + integration: new HttpProxyIntegration({ + url: 'https://get-books-proxy.myproxy.internal', + }), + path: '/books', + authorizer, +}); +``` + +### User Pool Authorizer + +User Pool Authorizer is a type of JWT Authorizer that uses a Cognito user pool and app client to control who can access your Api. After a successful authorization from the app client, the generated access token will be used as the JWT. + +Clients accessing an API that uses a user pool authorizer must first sign in to a user pool and obtain an identity or access token. +They must then use this token in the `Authorization` header of the API call. More information is available at [using Amazon Cognito user +pools as authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html). + +```ts +const userPool = new UserPool(stack, 'UserPool'); +const userPoolClient = userPool.addClient('UserPoolClient'); + +const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, +}); + +const api = new HttpApi(stack, 'HttpApi'); + +api.addRoutes({ + integration: new HttpProxyIntegration({ + url: 'https://get-books-proxy.myproxy.internal', + }), + path: '/books', + authorizer, +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/jest.config.js b/packages/@aws-cdk/aws-apigatewayv2-authorizers/jest.config.js new file mode 100644 index 0000000000000..b5ccdecc15ee0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 70, + }, + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/index.ts new file mode 100644 index 0000000000000..9f9ad94c6a4b7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/index.ts @@ -0,0 +1,2 @@ +export * from './user-pool'; +export * from './jwt'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/jwt.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/jwt.ts new file mode 100644 index 0000000000000..afb5f10ac07f8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/jwt.ts @@ -0,0 +1,70 @@ +import { + HttpAuthorizer, + HttpAuthorizerType, + HttpRouteAuthorizerBindOptions, + HttpRouteAuthorizerConfig, + IHttpRouteAuthorizer, +} from '@aws-cdk/aws-apigatewayv2'; +import { Token } from '@aws-cdk/core'; + +/** + * Properties to initialize HttpJwtAuthorizer. + */ +export interface HttpJwtAuthorizerProps { + + /** + * The name of the authorizer + * @default 'JwtAuthorizer' + */ + readonly authorizerName?: string; + + /** + * The identity source for which authorization is requested. + * + * @default ['$request.header.Authorization'] + */ + readonly identitySource?: string[], + + /** + * A list of the intended recipients of the JWT. + * A valid JWT must provide an aud that matches at least one entry in this list. + */ + readonly jwtAudience: string[] + + /** + * The base domain of the identity provider that issues JWT. + */ + readonly jwtIssuer: string; +} + +/** + * Authorize Http Api routes on whether the requester is registered as part of + * an AWS Cognito user pool. + */ +export class HttpJwtAuthorizer implements IHttpRouteAuthorizer { + private authorizer?: HttpAuthorizer; + + constructor(private readonly props: HttpJwtAuthorizerProps) { + } + + public bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + if (!this.authorizer) { + const id = this.props.authorizerName && !Token.isUnresolved(this.props.authorizerName) ? + this.props.authorizerName : 'JwtAuthorizer'; + + this.authorizer = new HttpAuthorizer(options.scope, id, { + httpApi: options.route.httpApi, + identitySource: this.props.identitySource ?? ['$request.header.Authorization'], + type: HttpAuthorizerType.JWT, + authorizerName: this.props.authorizerName, + jwtAudience: this.props.jwtAudience, + jwtIssuer: this.props.jwtIssuer, + }); + } + + return { + authorizerId: this.authorizer.authorizerId, + authorizationType: HttpAuthorizerType.JWT, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/user-pool.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/user-pool.ts new file mode 100644 index 0000000000000..4a251b8eb7406 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/user-pool.ts @@ -0,0 +1,69 @@ +import { HttpAuthorizer, HttpAuthorizerType, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, IHttpRouteAuthorizer } from '@aws-cdk/aws-apigatewayv2'; +import { IUserPool, IUserPoolClient } from '@aws-cdk/aws-cognito'; +import { Stack, Token } from '@aws-cdk/core'; + +/** + * Properties to initialize UserPoolAuthorizer. + */ +export interface UserPoolAuthorizerProps { + /** + * The user pool client that should be used to authorize requests with the user pool. + */ + readonly userPoolClient: IUserPoolClient; + + /** + * The associated user pool + */ + readonly userPool: IUserPool; + + /** + * The AWS region in which the user pool is present + * @default - same region as the Route the authorizer is attached to. + */ + readonly userPoolRegion?: string; + + /** + * The name of the authorizer + * @default 'UserPoolAuthorizer' + */ + readonly authorizerName?: string; + + /** + * The identity source for which authorization is requested. + * + * @default ['$request.header.Authorization'] + */ + readonly identitySource?: string[], +} + +/** + * Authorize Http Api routes on whether the requester is registered as part of + * an AWS Cognito user pool. + */ +export class HttpUserPoolAuthorizer implements IHttpRouteAuthorizer { + private authorizer?: HttpAuthorizer; + + constructor(private readonly props: UserPoolAuthorizerProps) { + } + + public bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + if (!this.authorizer) { + const id = this.props.authorizerName && !Token.isUnresolved(this.props.authorizerName) ? + this.props.authorizerName : 'UserPoolAuthorizer'; + const region = this.props.userPoolRegion ?? Stack.of(options.scope).region; + this.authorizer = new HttpAuthorizer(options.scope, id, { + httpApi: options.route.httpApi, + identitySource: this.props.identitySource ?? ['$request.header.Authorization'], + type: HttpAuthorizerType.JWT, + authorizerName: this.props.authorizerName, + jwtAudience: [this.props.userPoolClient.userPoolClientId], + jwtIssuer: `https://cognito-idp.${region}.amazonaws.com/${this.props.userPool.userPoolId}`, + }); + } + + return { + authorizerId: this.authorizer.authorizerId, + authorizationType: HttpAuthorizerType.JWT, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/index.ts new file mode 100644 index 0000000000000..c202386ae710e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/index.ts @@ -0,0 +1 @@ +export * from './http'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json new file mode 100644 index 0000000000000..de6dffe1edc61 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json @@ -0,0 +1,101 @@ +{ + "name": "@aws-cdk/aws-apigatewayv2-authorizers", + "version": "0.0.0", + "description": "Authorizers for AWS APIGateway V2", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.APIGatewayv2.Authorizers", + "packageId": "Amazon.CDK.AWS.APIGatewayv2.Authorizers", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.apigatewayv2.authorizers", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "apigatewayv2-authorizers" + } + }, + "python": { + "distName": "aws-cdk.aws-apigatewayv2-authorizers", + "module": "aws_cdk.aws_apigatewayv2_authorizers", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-apigatewayv2-authorizers" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "integ": "cdk-integ", + "lint": "cdk-lint", + "package": "cdk-package", + "awslint": "cdk-awslint", + "pkglint": "pkglint -f", + "test": "cdk-test", + "watch": "cdk-watch", + "compat": "cdk-compat", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "cdk-build": { + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "apigateway" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json new file mode 100644 index 0000000000000..ccdbd26a3cbd2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json @@ -0,0 +1,288 @@ +{ + "Resources": { + "MyHttpApi8AEAAC21": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "MyHttpApi", + "ProtocolType": "HTTP" + } + }, + "MyHttpApiDefaultStageDCB9BC49": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "MyHttpApiGETAuthorizerIntegMyHttpApiGET16D02385PermissionBB02EBFE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambda8B5974B5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyHttpApi8AEAAC21" + }, + "/*/*/" + ] + ] + } + } + }, + "MyHttpApiGETE0EFC6F8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "RouteKey": "GET /", + "AuthorizationScopes": [], + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "MyHttpApiUserPoolAuthorizer8754262B" + }, + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "MyHttpApiHttpIntegration6b0135d91888d914a9a0945a69f42c05C76E7A88" + } + ] + ] + } + } + }, + "MyHttpApiHttpIntegration6b0135d91888d914a9a0945a69f42c05C76E7A88": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "lambda8B5974B5", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "MyHttpApiUserPoolAuthorizer8754262B": { + "Type": "AWS::ApiGatewayV2::Authorizer", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "AuthorizerType": "JWT", + "IdentitySource": [ + "$request.header.Authorization" + ], + "Name": "UserPoolAuthorizer", + "JwtConfiguration": { + "Audience": [ + { + "Ref": "userpoolmyclientFAD947AB" + } + ], + "Issuer": { + "Fn::Join": [ + "", + [ + "https://cognito-idp.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/", + { + "Ref": "userpool0AC4AA96" + } + ] + ] + } + } + } + }, + "userpool0AC4AA96": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "userpoolmyclientFAD947AB": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "userpool0AC4AA96" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "lambdaServiceRole494E4CA6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "lambda8B5974B5": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3Bucket0AFE1748" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3VersionKey8E654BCC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3VersionKey8E654BCC" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "lambdaServiceRole494E4CA6", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "lambdaServiceRole494E4CA6" + ] + } + }, + "Parameters": { + "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3Bucket0AFE1748": { + "Type": "String", + "Description": "S3 bucket for asset \"7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cd\"" + }, + "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3VersionKey8E654BCC": { + "Type": "String", + "Description": "S3 key for asset version \"7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cd\"" + }, + "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdArtifactHashC4761AE9": { + "Type": "String", + "Description": "Artifact hash for asset \"7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cd\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.ts new file mode 100644 index 0000000000000..edf455f4a787c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.ts @@ -0,0 +1,42 @@ +/// !cdk-integ pragma:ignore-assets +import * as path from 'path'; +import { HttpApi, HttpMethod } from '@aws-cdk/aws-apigatewayv2'; +import { LambdaProxyIntegration } from '@aws-cdk/aws-apigatewayv2-integrations'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, Stack } from '@aws-cdk/core'; +import { HttpUserPoolAuthorizer } from '../../lib'; + +/* + * Stack verification steps: + * * `curl -s -o /dev/null -w "%{http_code}" ` should return 401 + * * `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' ` should return 403 + * * `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' ` should return 200 + */ + +const app = new App(); +const stack = new Stack(app, 'AuthorizerInteg'); + +const httpApi = new HttpApi(stack, 'MyHttpApi'); + +const userPool = new cognito.UserPool(stack, 'userpool'); + +const userPoolClient = userPool.addClient('my-client'); + +const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, +}); + +const handler = new lambda.Function(stack, 'lambda', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.AssetCode.fromAsset(path.join(__dirname, '../integ.user-pool.handler')), +}); + +httpApi.addRoutes({ + path: '/', + methods: [HttpMethod.GET], + integration: new LambdaProxyIntegration({ handler }), + authorizer, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/jwt.test.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/jwt.test.ts new file mode 100644 index 0000000000000..8b27dc312a1a3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/jwt.test.ts @@ -0,0 +1,70 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpIntegrationType, HttpRouteIntegrationBindOptions, IHttpRouteIntegration, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { Stack } from '@aws-cdk/core'; +import { HttpJwtAuthorizer } from '../../lib'; + +describe('HttpJwtAuthorizer', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + const authorizer = new HttpJwtAuthorizer({ + jwtAudience: ['3131231'], + jwtIssuer: 'https://test.us.auth0.com', + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + AuthorizerType: 'JWT', + IdentitySource: ['$request.header.Authorization'], + JwtConfiguration: { + Audience: ['3131231'], + Issuer: 'https://test.us.auth0.com', + }, + }); + }); + + test('same authorizer is used when bound to multiple routes', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + const authorizer = new HttpJwtAuthorizer({ + jwtAudience: ['3131231'], + jwtIssuer: 'https://test.us.auth0.com', + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/pets', + authorizer, + }); + + // THEN + expect(stack).toCountResources('AWS::ApiGatewayV2::Authorizer', 1); + }); +}); + +class DummyRouteIntegration implements IHttpRouteIntegration { + public bind(_: HttpRouteIntegrationBindOptions) { + return { + payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, + type: HttpIntegrationType.HTTP_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/user-pool.test.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/user-pool.test.ts new file mode 100644 index 0000000000000..c12e00a342acd --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/user-pool.test.ts @@ -0,0 +1,83 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpIntegrationType, HttpRouteIntegrationBindOptions, IHttpRouteIntegration, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { UserPool } from '@aws-cdk/aws-cognito'; +import { Stack } from '@aws-cdk/core'; +import { HttpUserPoolAuthorizer } from '../../lib'; + +describe('HttpUserPoolAuthorizer', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + const userPool = new UserPool(stack, 'UserPool'); + const userPoolClient = userPool.addClient('UserPoolClient'); + const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + AuthorizerType: 'JWT', + IdentitySource: ['$request.header.Authorization'], + JwtConfiguration: { + Audience: [stack.resolve(userPoolClient.userPoolClientId)], + Issuer: { + 'Fn::Join': [ + '', + [ + 'https://cognito-idp.', + { Ref: 'AWS::Region' }, + '.amazonaws.com/', + stack.resolve(userPool.userPoolId), + ], + ], + }, + }, + }); + }); + + test('same authorizer is used when bound to multiple routes', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + const userPool = new UserPool(stack, 'UserPool'); + const userPoolClient = userPool.addClient('UserPoolClient'); + const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/pets', + authorizer, + }); + + // THEN + expect(stack).toCountResources('AWS::ApiGatewayV2::Authorizer', 1); + }); +}); + +class DummyRouteIntegration implements IHttpRouteIntegration { + public bind(_: HttpRouteIntegrationBindOptions) { + return { + payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, + type: HttpIntegrationType.HTTP_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/integ.user-pool.handler/index.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/integ.user-pool.handler/index.ts new file mode 100644 index 0000000000000..afedb7efe3311 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/integ.user-pool.handler/index.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-console */ + +export const handler = async (event: any, _context: any = {}): Promise => { + const authToken: string = event.authorizationToken; + console.log(`event.authorizationToken = ${authToken}`); + if (authToken === 'allow' || authToken === 'deny') { + return { + principalId: 'user', + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: authToken, + Resource: event.methodArn, + }, + ], + }, + }; + } else { + throw new Error('Unauthorized'); + } +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json index d96b67d1582f9..cc969ddfa4684 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json @@ -615,6 +615,7 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json index 1c2bc834f7f48..709d1871e780e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json @@ -101,6 +101,7 @@ "Ref": "LambdaProxyApi67594471" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", @@ -154,6 +155,7 @@ "Ref": "HttpProxyApiD0217C67" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json index c2661f62833a8..99547f333ade2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json @@ -101,6 +101,7 @@ "Ref": "LambdaProxyApi67594471" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json index a29f632928938..5fa3047213025 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json @@ -580,6 +580,7 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json index d815552e3107d..39b8d98fafe0a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json @@ -581,6 +581,7 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 1fda5a731ff40..4da900f271e8f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -34,6 +34,7 @@ Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shie - [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Publishing HTTP APIs](#publishing-http-apis) - [Custom Domain](#custom-domain) + - [Managing access](#managing-access) - [Metrics](#metrics) - [VPC Link](#vpc-link) - [Private Integration](#private-integration) @@ -222,6 +223,13 @@ with 3 API mapping resources across different APIs and Stages. | api | beta | `https://${domainName}/bar` | | apiDemo | $default | `https://${domainName}/demo` | +### Managing access + +API Gateway supports multiple mechanisms for [controlling and managing access to your HTTP +API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html) through authorizers. + +These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library. + ## Metrics The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts new file mode 100644 index 0000000000000..609d469a572fa --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts @@ -0,0 +1,12 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents an Authorizer. + */ +export interface IAuthorizer extends IResource { + /** + * Id of the Authorizer + * @attribute + */ + readonly authorizerId: string +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index d727436b86c99..eeb237a4e7f84 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -3,3 +3,4 @@ export * from './route'; export * from './stage'; export * from './domain-name'; export * from './api-mapping'; +export * from './authorizer'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 94ea7991360ba..339efed0707fb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -4,7 +4,8 @@ import { Duration, IResource, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; import { DefaultDomainMappingOptions } from '../http/stage'; -import { HttpIntegration, HttpRouteIntegrationConfig, IHttpRouteIntegration } from './integration'; +import { IHttpRouteAuthorizer } from './authorizer'; +import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; import { HttpStage, HttpStageOptions } from './stage'; import { VpcLink, VpcLinkProps } from './vpc-link'; @@ -201,6 +202,20 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { * @default HttpMethod.ANY */ readonly methods?: HttpMethod[]; + + /** + * Authorizer to be associated to these routes. + * @default - No authorizer + */ + readonly authorizer?: IHttpRouteAuthorizer; + + /** + * The list of OIDC scopes to include in the authorization. + * + * These scopes will be merged with the scopes from the attached authorizer + * @default - no additional authorization scopes + */ + readonly authorizationScopes?: string[]; } abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported @@ -446,6 +461,8 @@ export class HttpApi extends HttpApiBase { httpApi: this, routeKey: HttpRouteKey.with(options.path, method), integration: options.integration, + authorizer: options.authorizer, + authorizationScopes: options.authorizationScopes, })); } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts new file mode 100644 index 0000000000000..aadfb630ba276 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts @@ -0,0 +1,174 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnAuthorizer } from '../apigatewayv2.generated'; + +import { IAuthorizer } from '../common'; +import { IHttpApi } from './api'; +import { IHttpRoute } from './route'; + +/** + * Supported Authorizer types + */ +export enum HttpAuthorizerType { + /** JSON Web Tokens */ + JWT = 'JWT', + + /** Lambda Authorizer */ + LAMBDA = 'REQUEST', +} + +/** + * Properties to initialize an instance of `HttpAuthorizer`. + */ +export interface HttpAuthorizerProps { + /** + * Name of the authorizer + * @default - id of the HttpAuthorizer construct. + */ + readonly authorizerName?: string + + /** + * HTTP Api to attach the authorizer to + */ + readonly httpApi: IHttpApi + + /** + * The type of authorizer + */ + readonly type: HttpAuthorizerType; + + /** + * The identity source for which authorization is requested. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-authorizer.html#cfn-apigatewayv2-authorizer-identitysource + */ + readonly identitySource: string[]; + + /** + * A list of the intended recipients of the JWT. + * A valid JWT must provide an aud that matches at least one entry in this list. + * @default - required for JWT authorizer typess. + */ + readonly jwtAudience?: string[] + + /** + * The base domain of the identity provider that issues JWT. + * @default - required for JWT authorizer types. + */ + readonly jwtIssuer?: string; +} + +/** + * An authorizer for HTTP APIs + */ +export interface IHttpAuthorizer extends IAuthorizer { +} + +/** + * Reference to an http authorizer + */ +export interface HttpAuthorizerAttributes { + /** + * Id of the Authorizer + */ + readonly authorizerId: string + + /** + * Type of authorizer + */ + readonly authorizerType: HttpAuthorizerType +} + +/** + * An authorizer for Http Apis + * @resource AWS::ApiGatewayV2::Authorizer + */ +export class HttpAuthorizer extends Resource implements IHttpAuthorizer { + /** + * Import an existing HTTP Authorizer into this CDK app. + */ + public static fromHttpAuthorizerAttributes(scope: Construct, id: string, attrs: HttpAuthorizerAttributes): IHttpRouteAuthorizer { + class Import extends Resource implements IHttpRouteAuthorizer { + public readonly authorizerId = attrs.authorizerId; + public readonly authorizerType = attrs.authorizerType; + + public bind(): HttpRouteAuthorizerConfig { + return { + authorizerId: attrs.authorizerId, + authorizationType: attrs.authorizerType, + }; + } + } + return new Import(scope, id); + } + + public readonly authorizerId: string; + + constructor(scope: Construct, id: string, props: HttpAuthorizerProps) { + super(scope, id); + + if (props.type === HttpAuthorizerType.JWT && (!props.jwtAudience || props.jwtAudience.length === 0 || !props.jwtIssuer)) { + throw new Error('jwtAudience and jwtIssuer are mandatory for JWT authorizers'); + } + + const resource = new CfnAuthorizer(this, 'Resource', { + name: props.authorizerName ?? id, + apiId: props.httpApi.httpApiId, + authorizerType: props.type, + identitySource: props.identitySource, + jwtConfiguration: undefinedIfNoKeys({ + audience: props.jwtAudience, + issuer: props.jwtIssuer, + }), + }); + + this.authorizerId = resource.ref; + } +} + +/** + * Input to the bind() operation, that binds an authorizer to a route. + */ +export interface HttpRouteAuthorizerBindOptions { + /** + * The route to which the authorizer is being bound. + */ + readonly route: IHttpRoute; + /** + * The scope for any constructs created as part of the bind. + */ + readonly scope: Construct; +} + +/** + * Results of binding an authorizer to an http route. + */ +export interface HttpRouteAuthorizerConfig { + /** + * The authorizer id + */ + readonly authorizerId: string; + /** + * The type of authorization + */ + readonly authorizationType: HttpAuthorizerType; + /** + * The list of OIDC scopes to include in the authorization. + * @default - no authorization scopes + */ + readonly authorizationScopes?: string[]; +} + +/** + * An authorizer that can attach to an Http Route. + */ +export interface IHttpRouteAuthorizer { + /** + * Bind this authorizer to a specified Http route. + */ + bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig; +} + +function undefinedIfNoKeys(obj: A): A | undefined { + const allUndefined = Object.values(obj).every(val => val === undefined); + return allUndefined ? undefined : obj; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index c594da33bac91..efd60f9f24d7c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -4,3 +4,4 @@ export * from './integration'; export * from './stage'; export * from './api-mapping'; export * from './vpc-link'; +export * from './authorizer'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index 27a0d1d7584a0..dac962f7bfee4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -3,6 +3,7 @@ import { Construct } from 'constructs'; import { CfnRoute, CfnRouteProps } from '../apigatewayv2.generated'; import { IRoute } from '../common'; import { IHttpApi } from './api'; +import { IHttpRouteAuthorizer } from './authorizer'; import { IHttpRouteIntegration } from './integration'; /** @@ -103,6 +104,20 @@ export interface HttpRouteProps extends BatchHttpRouteOptions { * The key to this route. This is a combination of an HTTP method and an HTTP path. */ readonly routeKey: HttpRouteKey; + + /** + * Authorizer for a WebSocket API or an HTTP API. + * @default - No authorizer + */ + readonly authorizer?: IHttpRouteAuthorizer; + + /** + * The list of OIDC scopes to include in the authorization. + * + * These scopes will be merged with the scopes from the attached authorizer + * @default - no additional authorization scopes + */ + readonly authorizationScopes?: string[]; } /** @@ -127,10 +142,27 @@ export class HttpRoute extends Resource implements IHttpRoute { const integration = props.httpApi._addIntegration(config); + const authBindResult = props.authorizer ? props.authorizer.bind({ + route: this, + scope: this.httpApi instanceof Construct ? this.httpApi : this, // scope under the API if it's not imported + }) : undefined; + + let authorizationScopes = authBindResult?.authorizationScopes ?? []; + + if (authBindResult && props.authorizationScopes) { + authorizationScopes = Array.from(new Set([ + ...authorizationScopes, + ...props.authorizationScopes, + ])); + } + const routeProps: CfnRouteProps = { apiId: props.httpApi.httpApiId, routeKey: props.routeKey.key, target: `integrations/${integration.integrationId}`, + authorizerId: authBindResult?.authorizerId, + authorizationType: authBindResult?.authorizationType, + authorizationScopes, }; const route = new CfnRoute(this, 'Resource', routeProps); diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 68a56a7acb216..53cfaeefc2515 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -77,6 +77,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index 70bc45000ddec..01252be7d84f1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -3,7 +3,10 @@ import { ABSENT } from '@aws-cdk/assert'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; -import { HttpApi, HttpIntegrationType, HttpMethod, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../../lib'; +import { + HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, + HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, +} from '../../lib'; describe('HttpApi', () => { test('default', () => { @@ -274,6 +277,83 @@ describe('HttpApi', () => { expect(api.apiEndpoint).toBeDefined(); }); + test('can attach authorizer to route', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api'); + + const authorizer = new DummyAuthorizer(); + + httpApi.addRoutes({ + path: '/pets', + integration: new DummyRouteIntegration(), + authorizer, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'HTTP', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: 'auth-1234', + AuthorizationType: 'JWT', + }); + }); + + test('can import existing authorizer and attach to route', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + const authorizer = HttpAuthorizer.fromHttpAuthorizerAttributes(stack, 'auth', { + authorizerId: '12345', + authorizerType: HttpAuthorizerType.JWT, + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/pets', + authorizer, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: '12345', + }); + }); + + test('can attach custom scopes to authorizer route', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api'); + + const authorizer = new DummyAuthorizer(); + + httpApi.addRoutes({ + path: '/pets', + integration: new DummyRouteIntegration(), + authorizer, + authorizationScopes: ['read:scopes'], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'HTTP', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: 'auth-1234', + AuthorizationType: 'JWT', + AuthorizationScopes: ['read:scopes'], + }); + }); + test('throws when accessing apiEndpoint and disableExecuteApiEndpoint is true', () => { const stack = new Stack(); const api = new HttpApi(stack, 'api', { @@ -301,4 +381,13 @@ class DummyRouteIntegration implements IHttpRouteIntegration { uri: 'some-uri', }; } +} + +class DummyAuthorizer implements IHttpRouteAuthorizer { + public bind(_: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + return { + authorizerId: 'auth-1234', + authorizationType: HttpAuthorizerType.JWT, + }; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts new file mode 100644 index 0000000000000..bee2f7d05be1b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts @@ -0,0 +1,67 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + HttpApi, HttpAuthorizer, HttpAuthorizerType, +} from '../../lib'; + +describe('HttpAuthorizer', () => { + test('default', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpAuthorizer(stack, 'HttpAuthorizer', { + httpApi, + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + ApiId: stack.resolve(httpApi.httpApiId), + Name: 'HttpAuthorizer', + AuthorizerType: 'JWT', + IdentitySource: ['identitysource.1', 'identitysource.2'], + }); + }); + + test('authorizer name', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpAuthorizer(stack, 'HttpAuthorizer', { + httpApi, + authorizerName: 'my-authorizer', + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + Name: 'my-authorizer', + }); + }); + + describe('jwt configuration', () => { + test('audience and issuer', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpAuthorizer(stack, 'HttpAuthorizer', { + httpApi, + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + JwtConfiguration: { + Audience: ['audience.1', 'audience.2'], + Issuer: 'issuer', + }, + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 9ca235814cd37..bfeb036a3fa72 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,8 +1,8 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { - HttpApi, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteIntegration, - PayloadFormatVersion, + HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteAuthorizerBindOptions, + HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; describe('HttpRoute', () => { @@ -173,6 +173,53 @@ describe('HttpRoute', () => { }); expect(stack).not.toHaveResource('AWS::ApiGatewayV2::VpcLink'); }); + + test('can create route with an authorizer attached', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + const authorizer = new DummyAuthorizer(); + + const route = new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + authorizer, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(httpApi.httpApiId), + IntegrationType: 'HTTP_PROXY', + PayloadFormatVersion: '2.0', + IntegrationUri: 'some-uri', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer'); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: stack.resolve(authorizer.bind({ scope: stack, route: route }).authorizerId), + AuthorizationType: 'JWT', + }); + }); + + test('can attach additional scopes to a route with an authorizer attached', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + const authorizer = new DummyAuthorizer(); + + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + authorizer, + authorizationScopes: ['read:books'], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizationScopes: ['read:books'], + }); + }); }); class DummyIntegration implements IHttpRouteIntegration { @@ -184,4 +231,26 @@ class DummyIntegration implements IHttpRouteIntegration { method: HttpMethod.DELETE, }; } +} + +class DummyAuthorizer implements IHttpRouteAuthorizer { + private authorizer?: HttpAuthorizer; + + public bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + if (!this.authorizer) { + + this.authorizer = new HttpAuthorizer(options.scope, 'auth-1234', { + httpApi: options.route.httpApi, + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + } + + return { + authorizerId: this.authorizer.authorizerId, + authorizationType: HttpAuthorizerType.JWT, + }; + } } \ No newline at end of file diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index fda319d7104db..70a29966650f1 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -108,6 +108,7 @@ "@aws-cdk/aws-amplify": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-authorizers": "0.0.0", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index e030318880208..41d11481f3108 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -36,6 +36,7 @@ "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", + "@aws-cdk/aws-apigatewayv2-authorizers": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index f06d5a236d5e3..883fa9169c512 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -113,6 +113,7 @@ "@aws-cdk/aws-amplify": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-authorizers": "0.0.0", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0",