From 59fe395a5adcd35bd59c6d9c74f4a2606aec88b0 Mon Sep 17 00:00:00 2001 From: Steven Guggenheimer Date: Fri, 14 Jan 2022 07:37:56 -0600 Subject: [PATCH] feat(cognito): identity pools (#16190) Adds Identity Pool L2 Construct which to date has not been implemented. Since Identity Pool's can't be used without default auth and unauth roles, also incorporated the L1 CfnIdentityPoolRoleAttachment into the Construct. Contains unit and integration tests as well as fully updated ReadMe. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cognito-identitypool/.eslintrc.js | 3 + .../aws-cognito-identitypool/.gitignore | 19 + .../aws-cognito-identitypool/.npmignore | 28 + .../@aws-cdk/aws-cognito-identitypool/LICENSE | 201 ++++++ .../@aws-cdk/aws-cognito-identitypool/NOTICE | 2 + .../aws-cognito-identitypool/README.md | 356 ++++++++++ .../aws-cognito-identitypool/jest.config.js | 2 + .../lib/identitypool-role-attachment.ts | 203 ++++++ ...ypool-user-pool-authentication-provider.ts | 118 ++++ .../lib/identitypool.ts | 490 ++++++++++++++ .../aws-cognito-identitypool/lib/index.ts | 3 + .../aws-cognito-identitypool/package.json | 110 ++++ .../test/identitypool.test.ts | 611 ++++++++++++++++++ .../test/integ.identitypool.expected.json | 417 ++++++++++++ .../test/integ.identitypool.ts | 73 +++ packages/@aws-cdk/aws-cognito/README.md | 2 +- packages/aws-cdk-lib/package.json | 2 + packages/decdk/package.json | 1 + packages/monocdk/package.json | 1 + yarn.lock | 4 +- 20 files changed, 2643 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/.gitignore create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/.npmignore create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/LICENSE create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/NOTICE create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/README.md create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/jest.config.js create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/package.json create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json create mode 100644 packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts diff --git a/packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js b/packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cognito-identitypool/.gitignore b/packages/@aws-cdk/aws-cognito-identitypool/.gitignore new file mode 100644 index 0000000000000..d8a8561d50885 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/.npmignore b/packages/@aws-cdk/aws-cognito-identitypool/.npmignore new file mode 100644 index 0000000000000..aaabf1df59065 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ +!*.lit.ts \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/LICENSE b/packages/@aws-cdk/aws-cognito-identitypool/LICENSE new file mode 100644 index 0000000000000..82ad00bb02d0b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/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-2022 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-cognito-identitypool/NOTICE b/packages/@aws-cdk/aws-cognito-identitypool/NOTICE new file mode 100644 index 0000000000000..1e8442b37b61d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/README.md b/packages/@aws-cdk/aws-cognito-identitypool/README.md new file mode 100644 index 0000000000000..fb1fc6ef0c0df --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/README.md @@ -0,0 +1,356 @@ +# Amazon Cognito Identity Pool Construct Library + +> **Identity Pools are in a separate module while the API is being stabilized. Once we stabilize the module, they will** +**be included into the stable [aws-cognito](../aws-cognito) library. Please provide feedback on this experience by** +**creating an [issue here](https://github.com/aws/aws-cdk/issues/new/choose)** + + + +--- + +![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. + +--- + + + +[Amazon Cognito Identity Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html) enable you to grant your users access to other AWS services. + +Identity Pools are one of the two main components of [Amazon Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html), which provides authentication, authorization, and +user management for your web and mobile apps. Your users can sign in directly with a user name and password, or through +a third party such as Facebook, Amazon, Google or Apple. + +The other main component in Amazon Cognito is [user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html). User Pools are user directories that provide sign-up and +sign-in options for your app users. + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +## Table of Contents + +- [Identity Pools](#identity-pools) + - [Authenticated and Unauthenticated Identities](#authenticated-and-unauthenticated-identities) + - [Authentication Providers](#authentication-providers) + - [User Pool Authentication Provider](#user-pool-authentication-provider) + - [Server Side Token Check](#server-side-token-check) + - [Associating an External Provider Directly](#associating-an-external-provider-directly) + - [OpenIdConnect and Saml](#openid-connect-and-saml) + - [Custom Providers](#custom-providers) + - [Role Mapping](#role-mapping) + - [Provider Urls](#provider-urls) + - [Authentication Flow](#authentication-flow) + - [Cognito Sync](#cognito-sync) + - [Importing Identity Pools](#importing-identity-pools) + +## Identity Pools + +Identity pools provide temporary AWS credentials for users who are guests (unauthenticated) and for users who have been +authenticated and received a token. An identity pool is a store of user identity data specific to an account. + +Identity pools can be used in conjunction with Cognito User Pools or by accessing external federated identity providers +directly. Learn more at [Amazon Cognito Identity Pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html). + +### Authenticated and Unauthenticated Identities + +Identity pools define two types of identities: authenticated(`user`) and unauthenticated (`guest`). Every identity in +an identity pool is either authenticated or unauthenticated. Each identity pool has a default role for authenticated +identities, and a default role for unauthenticated identities. Absent other overriding rules (see below), these are the +roles that will be assumed by the corresponding users in the authentication process. + +A basic Identity Pool with minimal configuration has no required props, with default authenticated (user) and +unauthenticated (guest) roles applied to the identity pool: + +```ts +new cognito.IdentityPool(this, 'myIdentityPool'); +``` + +By default, both the authenticated and unauthenticated roles will have no permissions attached. Grant permissions +to roles using the public `authenticatedRole` and `unauthenticatedRole` properties: + +```ts +const identityPool = new cognito.IdentityPool(this, 'myIdentityPool'); +const table = new dynamodb.Table(this, 'MyTable'); + +// Grant permissions to authenticated users +table.grantReadWriteData(identityPool.authenticatedRole); +// Grant permissions to unauthenticated guest users +table.grantRead(identityPool.unauthenticatedRole); + +//Or add policy statements straight to the role +identityPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:*'], + resources: ['*'] +})); +``` + +The default roles can also be supplied in `IdentityPoolProps`: + +```ts +const stack = new Stack(); +const authenticatedRole = new Role(stack, 'authRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), +}); +const unauthenticatedRole = new Role(stack, 'unauthRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), +}); +const identityPool = new IdentityPool(stack, 'TestIdentityPoolActions', { + authenticatedRole, unauthenticatedRole +}); +``` + +### Authentication Providers + +Authenticated identities belong to users who are authenticated by a public login provider (Amazon Cognito user pools, +Login with Amazon, Sign in with Apple, Facebook, Google, SAML, or any OpenID Connect Providers) or a developer provider +(your own backend authentication process). + +[Authentication providers](https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html) can be associated with an Identity Pool by first associating them with a Cognito User Pool or by +associating the provider directly with the identity pool. + +#### User Pool Authentication Provider + +In order to attach a user pool to an identity pool as an authentication provider, the identity pool needs properties +from both the user pool and the user pool client. For this reason identity pools use a `UserPoolAuthenticationProvider` +to gather the necessary properties from the user pool constructs. + +```ts +const userPool = new cognito.UserPool(this, 'Pool'); + +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + userPools: [new UserPoolAuthenticationProvider({ userPool })], + }, +}); +``` + +User pools can also be associated with an identity pool after instantiation. The Identity Pool's `addUserPoolAuthentication` method +returns the User Pool Client that has been created: + +```ts +const userPool = new cognito.UserPool(this, 'Pool'); +const userPoolClient = identityPool.addUserPoolAuthentication({ + userPools: [new UserPoolAuthenticationProvider({ userPool })]; +}); +``` + +#### Server Side Token Check + +With the `IdentityPool` CDK Construct, by default the pool is configured to check with the integrated user pools to +make sure that the user has not been globally signed out or deleted before the identity pool provides an OIDC token or +AWS credentials for the user. + +If the user is signed out or deleted, the identity pool will return a 400 Not Authorized error. This setting can be +disabled, however, in several ways. + +Setting `disableServerSideTokenCheck` to true will change the default behavior to no server side token check. Learn +more [here](https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_CognitoIdentityProvider.html#CognitoIdentity-Type-CognitoIdentityProvider-ServerSideTokenCheck): + +```ts +const userPool = new cognito.UserPool(this, 'Pool'); +identityPool.addUserPoolAuthentication({ + userPool: new UserPoolAuthenticationProvider({ + userPool, + disableServerSideTokenCheck: true, + }), +}); +``` + +#### Associating an External Provider Directly + +One or more [external identity providers](https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html) can be associated with an identity pool directly using +`authenticationProviders`: + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + amazon: { + appId: 'amzn1.application.12312k3j234j13rjiwuenf', + }, + facebook: { + appId: '1234567890123', + }, + google: { + clientId: '12345678012.apps.googleusercontent.com', + } + apple: { + servicesId: 'com.myappleapp.auth', + }, + twitter: { + consumerKey: 'my-twitter-id', + consumerSecret: 'my-twitter-secret', + }, + }, +}); +``` + +To associate more than one provider of the same type with the identity pool, use User +Pools, OpenIdConnect, or SAML. Only one provider per external service can be attached directly to the identity pool. + +#### OpenId Connect and Saml + +[OpenID Connect](https://docs.aws.amazon.com/cognito/latest/developerguide/open-id.html) is an open standard for +authentication that is supported by a number of login providers. Amazon Cognito supports linking of identities with +OpenID Connect providers that are configured through [AWS Identity and Access Management](http://aws.amazon.com/iam/). + +An identity provider that supports [Security Assertion Markup Language 2.0 (SAML 2.0)](https://docs.aws.amazon.com/cognito/latest/developerguide/saml-identity-provider.html) can be used to provide a simple +onboarding flow for users. The SAML-supporting identity provider specifies the IAM roles that can be assumed by users +so that different users can be granted different sets of permissions. Associating an OpenId Connect or Saml provider +with an identity pool: + +```ts +const openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'my-openid-connect-provider', ...); +const samlProvider = new iam.SamlProvider(this, 'my-saml-provider', ...); + +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + openIdConnectProvider: openIdConnectProvider, + samlProvider: samlProvider, + } +}); +``` + +#### Custom Providers + +The identity pool's behavior can be customized further using custom [developer authenticated identities](https://docs.aws.amazon.com/cognito/latest/developerguide/developer-authenticated-identities.html). +With developer authenticated identities, users can be registered and authenticated via an existing authentication +process while still using Amazon Cognito to synchronize user data and access AWS resources. + +Like the supported external providers, though, only one custom provider can be directly associated with the identity +pool. + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + authenticationProviders: { + google: '12345678012.apps.googleusercontent.com', + openIdConnectProvider: openIdConnectProvider, + customProvider: 'my-custom-provider.example.com', + } +}); +``` + +### Role Mapping + +In addition to setting default roles for authenticated and unauthenticated users, identity pools can also be used to +define rules to choose the role for each user based on claims in the user's ID token by using Role Mapping. When using +role mapping, it's important to be aware of some of the permissions the role will need. An in depth +review of roles and role mapping can be found [here](https://docs.aws.amazon.com/cognito/latest/developerguide/role-based-access-control.html). + +Using a [token-based approach](https://docs.aws.amazon.com/cognito/latest/developerguide/role-based-access-control.html#using-tokens-to-assign-roles-to-users) to role mapping will allow mapped roles to be passed through the `cognito:roles` or +`cognito:preferred_role` claims from the identity provider: + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + useToken: true, + }], +}); +``` + +Using a rule-based approach to role mapping allows roles to be assigned based on custom claims passed from the identity provider: + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + // Assign specific roles to users based on whether or not the custom admin claim is passed from the identity provider + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + rules: [ + { + claim: 'custom:admin', + claimValue: 'admin', + mappedRole: adminRole, + }, + { + claim: 'custom:admin', + claimValue: 'admin', + matchType: RoleMappingMatchType.NOTEQUAL, + mappedRole: nonAdminRole, + } + ], + }], +}); +``` + +Role mappings can also be added after instantiation with the Identity Pool's `addRoleMappings` method: + +```ts +identityPool.addRoleMappings(myAddedRoleMapping1, myAddedRoleMapping2, myAddedRoleMapping3); +``` + +#### Provider Urls + +Role mappings must be associated with the url of an Identity Provider which can be supplied +`IdentityPoolProviderUrl`. Supported Providers have static Urls that can be used: + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.FACEBOOK, + useToken: true, + }], +}); +``` + +For identity providers that don't have static Urls, a custom Url or User Pool Client Url can be supplied: + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + roleMappings: [ + { + providerUrl: IdentityPoolProviderUrl.userPool('cognito-idp.my-idp-region.amazonaws.com/my-idp-region_abcdefghi:app_client_id'), + useToken: true, + }, + { + providerUrl: IdentityPoolProviderUrl.custom('my-custom-provider.com'), + useToken: true, + }, + ], +}); +``` + +See [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-identitypoolroleattachment-rolemapping.html#cfn-cognito-identitypoolroleattachment-rolemapping-identityprovider) for more information. + +### Authentication Flow + +Identity Pool [Authentication Flow](https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html) defaults to the enhanced, simplified flow. The Classic (basic) Authentication Flow +can also be implemented using `allowClassicFlow`: + +```ts +new cognito.IdentityPool(this, 'myidentitypool', { + identityPoolName: 'myidentitypool', + allowClassicFlow: true, +}); +``` + +### Cognito Sync + +It's now recommended to integrate [AWS AppSync](https://aws.amazon.com/appsync/) for synchronizing app data across devices, so +Cognito Sync features like `PushSync`, `CognitoEvents`, and `CognitoStreams` are not a part of `IdentityPool`. More +information can be found [here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-sync.html). + +### Importing Identity Pools + +You can import existing identity pools into your stack using Identity Pool static methods with the Identity Pool Id or +Arn: + +```ts +IdentityPool.fromIdentityPoolId(this, 'my-imported-identity-pool', + 'us-east-1:dj2823ryiwuhef937'); +IdentityPool.fromIdentityPoolArn(this, 'my-imported-identity-pool', + 'arn:aws:cognito-identity:us-east-1:123456789012:identitypool/us-east-1:dj2823ryiwuhef937'); +``` + diff --git a/packages/@aws-cdk/aws-cognito-identitypool/jest.config.js b/packages/@aws-cdk/aws-cognito-identitypool/jest.config.js new file mode 100644 index 0000000000000..3a2fd93a1228a --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts new file mode 100644 index 0000000000000..b3811d99de5f3 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-role-attachment.ts @@ -0,0 +1,203 @@ +import { + CfnIdentityPoolRoleAttachment, +} from '@aws-cdk/aws-cognito'; +import { + IRole, +} from '@aws-cdk/aws-iam'; +import { + Resource, + IResource, +} from '@aws-cdk/core'; +import { + Construct, +} from 'constructs'; +import { + IIdentityPool, + IdentityPoolProviderUrl, +} from './identitypool'; + +/** + * Represents an Identity Pool Role Attachment + */ +export interface IIdentityPoolRoleAttachment extends IResource { + /** + * Id of the Attachments Underlying Identity Pool + */ + readonly identityPoolId: string; +} + +/** + * Props for an Identity Pool Role Attachment + */ +export interface IdentityPoolRoleAttachmentProps { + + /** + * Id of the Attachments Underlying Identity Pool + */ + readonly identityPool: IIdentityPool; + + /** + * Default Authenticated (User) Role + * @default - No default authenticated role will be added + */ + readonly authenticatedRole?: IRole; + + /** + * Default Unauthenticated (Guest) Role + * @default - No default unauthenticated role will be added + */ + readonly unauthenticatedRole?: IRole; + + /** + * Rules for mapping roles to users + * @default - no Role Mappings + */ + readonly roleMappings?: IdentityPoolRoleMapping[]; +} + +/** + * Map roles to users in the identity pool based on claims from the Identity Provider + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-identitypoolroleattachment.html + */ +export interface IdentityPoolRoleMapping { + /** + * The url of the provider of for which the role is mapped + */ + readonly providerUrl: IdentityPoolProviderUrl; + + /** + * If true then mapped roles must be passed through the cognito:roles or cognito:preferred_role claims from identity provider. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/role-based-access-control.html#using-tokens-to-assign-roles-to-users + * + * @default false + */ + readonly useToken?: boolean; + + /** + * Allow for role assumption when results of role mapping are ambiguous + * @default false - Ambiguous role resolutions will lead to requester being denied + */ + readonly resolveAmbiguousRoles?: boolean; + + /** + * The claim and value that must be matched in order to assume the role. Required if useToken is false + * @default - No Rule Mapping Rule + */ + readonly rules?: RoleMappingRule[]; +} + +/** + * Types of matches allowed for Role Mapping + */ +export enum RoleMappingMatchType { + /** + * The Claim from the token must equal the given value in order for a match + */ + EQUALS = 'Equals', + + /** + * The Claim from the token must contain the given value in order for a match + */ + CONTAINS = 'Contains', + + /** + * The Claim from the token must start with the given value in order for a match + */ + STARTS_WITH = 'StartsWith', + + /** + * The Claim from the token must not equal the given value in order for a match + */ + NOTEQUAL = 'NotEqual', +} + +/** + * Represents an Identity Pool Role Attachment Role Mapping Rule + */ +export interface RoleMappingRule { + /** + * The key sent in the token by the federated identity provider. + */ + readonly claim: string; + + /** + * The Role to be assumed when Claim Value is matched. + */ + readonly mappedRole: IRole; + + /** + * The value of the claim that must be matched + */ + readonly claimValue: string; + + /** + * How to match with the Claim value + * @default RoleMappingMatchType.EQUALS + */ + readonly matchType?: RoleMappingMatchType +} + +/** + * Defines an Identity Pool Role Attachment + * + * @resource AWS::Cognito::IdentityPoolRoleAttachment + */ +export class IdentityPoolRoleAttachment extends Resource implements IIdentityPoolRoleAttachment { + /** + * Id of the underlying identity pool + */ + public readonly identityPoolId: string + + constructor(scope: Construct, id: string, props: IdentityPoolRoleAttachmentProps) { + super(scope, id); + this.identityPoolId = props.identityPool.identityPoolId; + const mappings = props.roleMappings || []; + let roles: any = undefined, roleMappings: any = undefined; + if (props.authenticatedRole || props.unauthenticatedRole) { + roles = {}; + if (props.authenticatedRole) roles.authenticated = props.authenticatedRole.roleArn; + if (props.unauthenticatedRole) roles.unauthenticated = props.unauthenticatedRole.roleArn; + } + if (mappings) { + roleMappings = this.configureRoleMappings(...mappings); + } + new CfnIdentityPoolRoleAttachment(this, 'Resource', { + identityPoolId: this.identityPoolId, + roles, + roleMappings, + }); + } + + /** + * Configures Role Mappings for Identity Pool Role Attachment + */ + private configureRoleMappings( + ...props: IdentityPoolRoleMapping[] + ): { [name:string]: CfnIdentityPoolRoleAttachment.RoleMappingProperty } | undefined { + if (!props || !props.length) return undefined; + return props.reduce((acc, prop) => { + let roleMapping: any = { + ambiguousRoleResolution: prop.resolveAmbiguousRoles ? 'AuthenticatedRole' : 'Deny', + type: prop.useToken ? 'Token' : 'Rules', + identityProvider: prop.providerUrl.value, + }; + if (roleMapping.type === 'Rules') { + if (!prop.rules) { + throw new Error('IdentityPoolRoleMapping.rules is required when useToken is false'); + } + roleMapping.rulesConfiguration = { + rules: prop.rules.map(rule => { + return { + claim: rule.claim, + value: rule.claimValue, + matchType: rule.matchType || RoleMappingMatchType.EQUALS, + roleArn: rule.mappedRole.roleArn, + }; + }), + }; + }; + acc[prop.providerUrl.value] = roleMapping; + return acc; + }, {} as { [name:string]: CfnIdentityPoolRoleAttachment.RoleMappingProperty }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts new file mode 100644 index 0000000000000..831d223250258 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool-user-pool-authentication-provider.ts @@ -0,0 +1,118 @@ +import { + IUserPool, + IUserPoolClient, +} from '@aws-cdk/aws-cognito'; +import { Stack } from '@aws-cdk/core'; +import { + Construct, Node, +} from 'constructs'; +import { IIdentityPool } from './identitypool'; + +/** + * Represents the concept of a User Pool Authentication Provider. + * You use user pool authentication providers to configure User Pools + * and User Pool Clients for use with Identity Pools + */ +export interface IUserPoolAuthenticationProvider { + /** + * The method called when a given User Pool Authentication Provider is added + * (for the first time) to an Identity Pool. + */ + bind( + scope: Construct, + identityPool: IIdentityPool, + options?: UserPoolAuthenticationProviderBindOptions + ): UserPoolAuthenticationProviderBindConfig; +} + +/** + * Props for the User Pool Authentication Provider + */ +export interface UserPoolAuthenticationProviderProps { + /** + * The User Pool of the Associated Identity Providers + */ + readonly userPool: IUserPool; + + /** + * The User Pool Client for the provided User Pool + * @default - A default user pool client will be added to User Pool + */ + readonly userPoolClient?: IUserPoolClient; + + /** + * Setting this to true turns off identity pool checks for this user pool to make sure the user has not been globally signed out or deleted before the identity pool provides an OIDC token or AWS credentials for the user + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-identitypool-cognitoidentityprovider.html + * @default false + */ + readonly disableServerSideTokenCheck?: boolean; + +} + +/** + * Represents UserPoolAuthenticationProvider Bind Options + */ +export interface UserPoolAuthenticationProviderBindOptions {} + +/** + * Represents a UserPoolAuthenticationProvider Bind Configuration + */ +export interface UserPoolAuthenticationProviderBindConfig { + /** + * Client Id of the Associated User Pool Client + */ + readonly clientId: string + + /** + * The identity providers associated with the UserPool + */ + readonly providerName: string; + + /** + * Whether to enable the identity pool's server side token check + */ + readonly serverSideTokenCheck: boolean; +} + +/** + * Defines a User Pool Authentication Provider + */ +export class UserPoolAuthenticationProvider implements IUserPoolAuthenticationProvider { + + /** + * The User Pool of the Associated Identity Providers + */ + private userPool: IUserPool; + + /** + * The User Pool Client for the provided User Pool + */ + private userPoolClient: IUserPoolClient; + + /** + * Whether to disable the pool's default server side token check + */ + private disableServerSideTokenCheck: boolean + constructor(props: UserPoolAuthenticationProviderProps) { + this.userPool = props.userPool; + this.userPoolClient = props.userPoolClient || this.userPool.addClient('UserPoolAuthenticationProviderClient'); + this.disableServerSideTokenCheck = props.disableServerSideTokenCheck ?? false; + } + + public bind( + scope: Construct, + identityPool: IIdentityPool, + _options?: UserPoolAuthenticationProviderBindOptions, + ): UserPoolAuthenticationProviderBindConfig { + Node.of(identityPool).addDependency(this.userPool); + Node.of(identityPool).addDependency(this.userPoolClient); + const region = Stack.of(scope).region; + const urlSuffix = Stack.of(scope).urlSuffix; + + return { + clientId: this.userPoolClient.userPoolClientId, + providerName: `cognito-idp.${region}.${urlSuffix}/${this.userPool.userPoolId}`, + serverSideTokenCheck: !this.disableServerSideTokenCheck, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts new file mode 100644 index 0000000000000..cefab0caa54d9 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts @@ -0,0 +1,490 @@ +import { + CfnIdentityPool, +} from '@aws-cdk/aws-cognito'; +import { + IOpenIdConnectProvider, + ISamlProvider, + Role, + FederatedPrincipal, + IRole, +} from '@aws-cdk/aws-iam'; +import { + Resource, + IResource, + Stack, + ArnFormat, + Lazy, +} from '@aws-cdk/core'; +import { + Construct, +} from 'constructs'; +import { + IdentityPoolRoleAttachment, + IdentityPoolRoleMapping, +} from './identitypool-role-attachment'; +import { + IUserPoolAuthenticationProvider, +} from './identitypool-user-pool-authentication-provider'; + +/** + * Represents a Cognito IdentityPool + */ +export interface IIdentityPool extends IResource { + /** + * The id of the Identity Pool in the format REGION:GUID + * @attribute + */ + readonly identityPoolId: string; + + /** + * The ARN of the Identity Pool + * @attribute + */ + readonly identityPoolArn: string; + + /** + * Name of the Identity Pool + * @attribute + */ + readonly identityPoolName: string +} + +/** + * Props for the IdentityPool construct + */ +export interface IdentityPoolProps { + + /** + * The name of the Identity Pool + * @default - automatically generated name by CloudFormation at deploy time + */ + readonly identityPoolName?: string; + + /** + * The Default Role to be assumed by Authenticated Users + * @default - A Default Authenticated Role will be added + */ + readonly authenticatedRole?: IRole; + + /** + * The Default Role to be assumed by Unauthenticated Users + * @default - A Default Unauthenticated Role will be added + */ + readonly unauthenticatedRole?: IRole; + + /** + * Wwhether the identity pool supports unauthenticated logins + * @default - false + */ + readonly allowUnauthenticatedIdentities?: boolean + + /** + * Rules for mapping roles to users + * @default - no Role Mappings + */ + readonly roleMappings?: IdentityPoolRoleMapping[]; + + /** + * Enables the Basic (Classic) authentication flow + * @default - Classic Flow not allowed + */ + readonly allowClassicFlow?: boolean; + + /** + * Authentication providers for using in identity pool. + * @default - No Authentication Providers passed directly to Identity Pool + */ + readonly authenticationProviders?: IdentityPoolAuthenticationProviders +} + +/** + * Types of Identity Pool Login Providers + */ +export enum IdentityPoolProviderType { + /** Facebook Provider type */ + FACEBOOK = 'Facebook', + /** Google Provider Type */ + GOOGLE = 'Google', + /** Amazon Provider Type */ + AMAZON = 'Amazon', + /** Apple Provider Type */ + APPLE = 'Apple', + /** Twitter Provider Type */ + TWITTER = 'Twitter', + /** Digits Provider Type */ + DIGITS = 'Digits', + /** Open Id Provider Type */ + OPEN_ID = 'OpenId', + /** Saml Provider Type */ + SAML = 'Saml', + /** User Pool Provider Type */ + USER_POOL = 'UserPool', + /** Custom Provider Type */ + CUSTOM = 'Custom', +} + +/** + * Keys for Login Providers - correspond to client id's of respective federation identity providers + */ +export class IdentityPoolProviderUrl { + /** Facebook Provider Url */ + public static readonly FACEBOOK = new IdentityPoolProviderUrl(IdentityPoolProviderType.FACEBOOK, 'graph.facebook.com'); + + /** Google Provider Url */ + public static readonly GOOGLE = new IdentityPoolProviderUrl(IdentityPoolProviderType.GOOGLE, 'accounts.google.com'); + + /** Amazon Provider Url */ + public static readonly AMAZON = new IdentityPoolProviderUrl(IdentityPoolProviderType.AMAZON, 'www.amazon.com'); + + /** Apple Provider Url */ + public static readonly APPLE = new IdentityPoolProviderUrl(IdentityPoolProviderType.APPLE, 'appleid.apple.com'); + + /** Twitter Provider Url */ + public static readonly TWITTER = new IdentityPoolProviderUrl(IdentityPoolProviderType.TWITTER, 'api.twitter.com'); + + /** Digits Provider Url */ + public static readonly DIGITS = new IdentityPoolProviderUrl(IdentityPoolProviderType.DIGITS, 'www.digits.com'); + + /** OpenId Provider Url */ + public static openId(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.OPEN_ID, url); + } + + /** Saml Provider Url */ + public static saml(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.SAML, url); + } + + /** User Pool Provider Url */ + public static userPool(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.USER_POOL, url); + } + + /** Custom Provider Url */ + public static custom(url: string): IdentityPoolProviderUrl { + return new IdentityPoolProviderUrl(IdentityPoolProviderType.CUSTOM, url); + } + + constructor( + /** type of Provider Url */ + public readonly type: IdentityPoolProviderType, + /** value of Provider Url */ + public readonly value: string, + ) {} +} + +/** + * Login Provider for Identity Federation using Amazon Credentials + */ +export interface IdentityPoolAmazonLoginProvider { + /** + * App Id for Amazon Identity Federation + */ + readonly appId: string +} + +/** + * Login Provider for Identity Federation using Facebook Credentials + */ +export interface IdentityPoolFacebookLoginProvider { + /** + * App Id for Facebook Identity Federation + */ + readonly appId: string +} + +/** + * Login Provider for Identity Federation using Apple Credentials +*/ +export interface IdentityPoolAppleLoginProvider { + /** + * App Id for Apple Identity Federation + */ + readonly servicesId: string +} + +/** + * Login Provider for Identity Federation using Google Credentials + */ +export interface IdentityPoolGoogleLoginProvider { + /** + * App Id for Google Identity Federation + */ + readonly clientId: string +} + +/** + * Login Provider for Identity Federation using Twitter Credentials + */ +export interface IdentityPoolTwitterLoginProvider { + /** + * App Id for Twitter Identity Federation + */ + readonly consumerKey: string + + /** + * App Secret for Twitter Identity Federation + */ + readonly consumerSecret: string +} + +/** + * Login Provider for Identity Federation using Digits Credentials + */ +export interface IdentityPoolDigitsLoginProvider extends IdentityPoolTwitterLoginProvider {} + +/** + * External Identity Providers To Connect to User Pools and Identity Pools + */ +export interface IdentityPoolProviders { + + /** App Id for Facebook Identity Federation + * @default - No Facebook Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly facebook?: IdentityPoolFacebookLoginProvider + + /** Client Id for Google Identity Federation + * @default - No Google Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly google?: IdentityPoolGoogleLoginProvider + + /** App Id for Amazon Identity Federation + * @default - No Amazon Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly amazon?: IdentityPoolAmazonLoginProvider + + /** Services Id for Apple Identity Federation + * @default - No Apple Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly apple?: IdentityPoolAppleLoginProvider + + /** Consumer Key and Secret for Twitter Identity Federation + * @default - No Twitter Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly twitter?: IdentityPoolTwitterLoginProvider + + /** Consumer Key and Secret for Digits Identity Federation + * @default - No Digits Authentication Provider used without OpenIdConnect or a User Pool + */ + readonly digits?: IdentityPoolDigitsLoginProvider +} + +/** +* Authentication providers for using in identity pool. +* @see https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html +*/ +export interface IdentityPoolAuthenticationProviders extends IdentityPoolProviders { + + /** + * The User Pool Authentication Providers associated with this Identity Pool + * @default - no User Pools Associated + */ + readonly userPools?: IUserPoolAuthenticationProvider[]; + + /** + * The OpenIdConnect Provider associated with this Identity Pool + * @default - no OpenIdConnectProvider + */ + readonly openIdConnectProviders?: IOpenIdConnectProvider[]; + + /** + * The Security Assertion Markup Language Provider associated with this Identity Pool + * @default - no SamlProvider + */ + readonly samlProviders?: ISamlProvider[]; + + /** + * The Developer Provider Name to associate with this Identity Pool + * @default - no Custom Provider + */ + readonly customProvider?: string; +} + +/** + * Define a Cognito Identity Pool + * + * @resource AWS::Cognito::IdentityPool + */ +export class IdentityPool extends Resource implements IIdentityPool { + + /** + * Import an existing Identity Pool from its id + */ + public static fromIdentityPoolId(scope: Construct, id: string, identityPoolId: string): IIdentityPool { + const identityPoolArn = Stack.of(scope).formatArn({ + service: 'cognito-identity', + resource: 'identitypool', + resourceName: identityPoolId, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }); + + return IdentityPool.fromIdentityPoolArn(scope, id, identityPoolArn); + } + + /** + * Import an existing Identity Pool from its Arn + */ + public static fromIdentityPoolArn(scope: Construct, id: string, identityPoolArn: string): IIdentityPool { + const pool = Stack.of(scope).splitArn(identityPoolArn, ArnFormat.SLASH_RESOURCE_NAME); + const res = pool.resourceName || ''; + if (!res) { + throw new Error('Invalid Identity Pool ARN'); + } + const idParts = res.split(':'); + if (!(idParts.length === 2)) throw new Error('Invalid Identity Pool Id: Identity Pool Ids must follow the format :'); + if (idParts[0] !== pool.region) throw new Error('Invalid Identity Pool Id: Region in Identity Pool Id must match stack region'); + class ImportedIdentityPool extends Resource implements IIdentityPool { + public readonly identityPoolId = res; + public readonly identityPoolArn = identityPoolArn; + public readonly identityPoolName: string + constructor() { + super(scope, id, { + account: pool.account, + region: pool.region, + }); + this.identityPoolName = this.physicalName; + } + } + return new ImportedIdentityPool(); + } + + /** + * The id of the Identity Pool in the format REGION:GUID + * @attribute + */ + public readonly identityPoolId: string; + + /** + * The ARN of the Identity Pool + * @attribute + */ + public readonly identityPoolArn: string; + + /** + * The name of the Identity Pool + * @attribute + */ + public readonly identityPoolName: string; + + /** + * Default role for authenticated users + */ + public readonly authenticatedRole: IRole; + + /** + * Default role for unauthenticated users + */ + public readonly unauthenticatedRole: IRole; + + /** + * List of Identity Providers added in constructor for use with property overrides + */ + private cognitoIdentityProviders: CfnIdentityPool.CognitoIdentityProviderProperty[] = []; + + /** + * Running count of added role attachments + */ + private roleAttachmentCount: number = 0; + + constructor(scope: Construct, id: string, props:IdentityPoolProps = {}) { + super(scope, id, { + physicalName: props.identityPoolName, + }); + const authProviders: IdentityPoolAuthenticationProviders = props.authenticationProviders || {}; + const providers = authProviders.userPools ? authProviders.userPools.map(userPool => userPool.bind(this, this)) : undefined; + if (providers && providers.length) this.cognitoIdentityProviders = providers; + const openIdConnectProviderArns = authProviders.openIdConnectProviders ? + authProviders.openIdConnectProviders.map(openIdProvider => + openIdProvider.openIdConnectProviderArn, + ) : undefined; + const samlProviderArns = authProviders.samlProviders ? + authProviders.samlProviders.map(samlProvider => + samlProvider.samlProviderArn, + ) : undefined; + + let supportedLoginProviders:any = {}; + if (authProviders.amazon) supportedLoginProviders[IdentityPoolProviderUrl.AMAZON.value] = authProviders.amazon.appId; + if (authProviders.facebook) supportedLoginProviders[IdentityPoolProviderUrl.FACEBOOK.value] = authProviders.facebook.appId; + if (authProviders.google) supportedLoginProviders[IdentityPoolProviderUrl.GOOGLE.value] = authProviders.google.clientId; + if (authProviders.apple) supportedLoginProviders[IdentityPoolProviderUrl.APPLE.value] = authProviders.apple.servicesId; + if (authProviders.twitter) supportedLoginProviders[IdentityPoolProviderUrl.TWITTER.value] = `${authProviders.twitter.consumerKey};${authProviders.twitter.consumerSecret}`; + if (authProviders.digits) supportedLoginProviders[IdentityPoolProviderUrl.DIGITS.value] = `${authProviders.digits.consumerKey};${authProviders.digits.consumerSecret}`; + if (!Object.keys(supportedLoginProviders).length) supportedLoginProviders = undefined; + + const cfnIdentityPool = new CfnIdentityPool(this, 'Resource', { + allowUnauthenticatedIdentities: props.allowUnauthenticatedIdentities ? true : false, + allowClassicFlow: props.allowClassicFlow, + identityPoolName: this.physicalName, + developerProviderName: authProviders.customProvider, + openIdConnectProviderArns, + samlProviderArns, + supportedLoginProviders, + cognitoIdentityProviders: Lazy.any({ produce: () => this.cognitoIdentityProviders }), + }); + this.identityPoolName = cfnIdentityPool.attrName; + this.identityPoolId = cfnIdentityPool.ref; + this.identityPoolArn = Stack.of(scope).formatArn({ + service: 'cognito-identity', + resource: 'identitypool', + resourceName: this.identityPoolId, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }); + this.authenticatedRole = props.authenticatedRole ? props.authenticatedRole : this.configureDefaultRole('Authenticated'); + this.unauthenticatedRole = props.unauthenticatedRole ? props.unauthenticatedRole : this.configureDefaultRole('Unauthenticated'); + const attachment = new IdentityPoolRoleAttachment(this, 'DefaultRoleAttachment', { + identityPool: this, + authenticatedRole: this.authenticatedRole, + unauthenticatedRole: this.unauthenticatedRole, + roleMappings: props.roleMappings, + }); + attachment.node.addDependency(this); + } + + /** + * Add a User Pool to the IdentityPool and configure User Pool Client to handle identities + */ + public addUserPoolAuthentication(userPool: IUserPoolAuthenticationProvider): void { + const providers = userPool.bind(this, this); + this.cognitoIdentityProviders = this.cognitoIdentityProviders.concat(providers); + } + + /** + * Adds Role Mappings to Identity Pool + */ + public addRoleMappings(...roleMappings: IdentityPoolRoleMapping[]): void { + if (!roleMappings || !roleMappings.length) return; + this.roleAttachmentCount++; + const name = 'RoleMappingAttachment' + this.roleAttachmentCount.toString(); + const attachment = new IdentityPoolRoleAttachment(this, name, { + identityPool: this, + authenticatedRole: this.authenticatedRole, + unauthenticatedRole: this.unauthenticatedRole, + roleMappings, + }); + attachment.node.addDependency(this); + } + + /** + * Configure Default Roles For Identity Pool + */ + private configureDefaultRole(type: string): IRole { + const assumedBy = this.configureDefaultGrantPrincipal(type.toLowerCase()); + const role = new Role(this, `${type}Role`, { + description: `Default ${type} Role for Identity Pool ${this.identityPoolName}`, + assumedBy, + }); + + return role; + } + + private configureDefaultGrantPrincipal(type: string) { + return new FederatedPrincipal('cognito-identity.amazonaws.com', { + 'StringEquals': { + 'cognito-identity.amazonaws.com:aud': this.identityPoolId, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': type, + }, + }, 'sts:AssumeRoleWithWebIdentity'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts new file mode 100644 index 0000000000000..f24f1f580225b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/index.ts @@ -0,0 +1,3 @@ +export * from './identitypool'; +export * from './identitypool-role-attachment'; +export * from './identitypool-user-pool-authentication-provider'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/package.json b/packages/@aws-cdk/aws-cognito-identitypool/package.json new file mode 100644 index 0000000000000..e7b9183ce8ba7 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/package.json @@ -0,0 +1,110 @@ +{ + "name": "@aws-cdk/aws-cognito-identitypool", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::Cognito Identity Pools", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.cognito.identitypool", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cognito-identitypool" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.Cognito.IdentityPool", + "packageId": "Amazon.CDK.AWS.Cognito.IdentityPool", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-cognito-identitypool", + "module": "aws_cdk.aws_cognito_identitypool", + "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-cognito-identitypool" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "yarn build+test && yarn package", + "build+test": "yarn build && yarn test", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "cdk-build": { + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "cognito", + "identitypool" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cdk-integ-tools": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^27.0.3", + "jest": "^27.4.5" + }, + "dependencies": { + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "awslint": { + "exclude": [ + "no-unused-type:@aws-cdk/aws-cognito.IdentityPoolProviderType", + "props-physical-name:@aws-cdk/aws-cognito-identitypool.IdentityPoolRoleAttachmentProps" + ] + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts b/packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts new file mode 100644 index 0000000000000..903c3915a0e11 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/identitypool.test.ts @@ -0,0 +1,611 @@ +import { + Template, +} from '@aws-cdk/assertions'; +import { + UserPool, + UserPoolIdentityProvider, +} from '@aws-cdk/aws-cognito'; +import { + Role, + ServicePrincipal, + ArnPrincipal, + AnyPrincipal, + OpenIdConnectProvider, + SamlProvider, + SamlMetadataDocument, + PolicyStatement, + Effect, + PolicyDocument, +} from '@aws-cdk/aws-iam'; +import { + Stack, +} from '@aws-cdk/core'; +import { + IdentityPool, + IdentityPoolProviderUrl, +} from '../lib/identitypool'; +import { + RoleMappingMatchType, +} from '../lib/identitypool-role-attachment'; +import { UserPoolAuthenticationProvider } from '../lib/identitypool-user-pool-authentication-provider'; + +describe('identity pool', () => { + test('minimal setup', () => { + const stack = new Stack(); + new IdentityPool(stack, 'TestIdentityPoolMinimal'); + const temp = Template.fromStack(stack); + + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: false, + }); + + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.resourceCountIs('AWS::IAM::Policy', 0); + temp.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'StringEquals': { + 'cognito-identity.amazonaws.com:aud': { + Ref: 'TestIdentityPoolMinimal44837852', + }, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'authenticated', + }, + }, + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + }, + ], + }, + }); + + temp.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + 'StringEquals': { + 'cognito-identity.amazonaws.com:aud': { + Ref: 'TestIdentityPoolMinimal44837852', + }, + }, + 'ForAnyValue:StringLike': { + 'cognito-identity.amazonaws.com:amr': 'unauthenticated', + }, + }, + Effect: 'Allow', + Principal: { + Federated: 'cognito-identity.amazonaws.com', + }, + }, + ], + }, + }); + }); + + test('providing default roles directly', () => { + const stack = new Stack(); + const authenticatedRole = new Role(stack, 'authRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), + }); + const unauthenticatedRole = new Role(stack, 'unauthRole', { + assumedBy: new ServicePrincipal('service.amazonaws.com'), + }); + const identityPool = new IdentityPool(stack, 'TestIdentityPoolActions', { + authenticatedRole, unauthenticatedRole, allowUnauthenticatedIdentities: true, + }); + identityPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*', 'dynamodb:*'], + resources: ['*'], + })); + identityPool.unauthenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*'], + resources: ['arn:aws:execute-api:us-east-1:*:my-api/prod'], + })); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.resourceCountIs('AWS::IAM::Policy', 2); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: true, + }); + temp.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'service.amazonaws.com', + }, + }, + ], + }, + }); + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'execute-api:*', + Effect: 'Allow', + Resource: 'arn:aws:execute-api:us-east-1:*:my-api/prod', + }, + ], + }, + }); + + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['execute-api:*', 'dynamodb:*'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + }); + test('adding actions and resources to default roles', () => { + const stack = new Stack(); + const identityPool = new IdentityPool(stack, 'TestIdentityPoolActions'); + identityPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*', 'dynamodb:*'], + resources: ['*'], + })); + identityPool.unauthenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['execute-api:*'], + resources: ['arn:aws:execute-api:us-east-1:*:my-api/prod'], + })); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.resourceCountIs('AWS::IAM::Policy', 2); + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'execute-api:*', + Effect: 'Allow', + Resource: 'arn:aws:execute-api:us-east-1:*:my-api/prod', + }, + ], + }, + }); + + temp.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['execute-api:*', 'dynamodb:*'], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + }); + + test('from static', () => { + const stack = new Stack(undefined, undefined, { + env: { + region: 'my-region', + account: '1234567891011', + }, + }); + expect(() => IdentityPool.fromIdentityPoolId(stack, 'idPoolIdError', 'idPool')).toThrowError('Invalid Identity Pool Id: Identity Pool Ids must follow the format :'); + expect(() => IdentityPool.fromIdentityPoolArn(stack, 'idPoolArnError', 'arn:aws:cognito-identity:my-region:1234567891011:identitypool\/your-region:idPool/')).toThrowError('Invalid Identity Pool Id: Region in Identity Pool Id must match stack region'); + const idPool = IdentityPool.fromIdentityPoolId(stack, 'staticIdPool', 'my-region:idPool'); + + expect(idPool.identityPoolId).toEqual('my-region:idPool'); + expect(idPool.identityPoolArn).toMatch(/cognito-identity:my-region:1234567891011:identitypool\/my-region:idPool/); + }); + + test('user pools are properly configured', () => { + const stack = new Stack(); + const poolProvider = UserPoolIdentityProvider.fromProviderName(stack, 'poolProvider', 'poolProvider'); + const otherPoolProvider = UserPoolIdentityProvider.fromProviderName(stack, 'otherPoolProvider', 'otherPoolProvider'); + const pool = new UserPool(stack, 'Pool'); + const otherPool = new UserPool(stack, 'OtherPool'); + pool.registerIdentityProvider(poolProvider); + otherPool.registerIdentityProvider(otherPoolProvider); + const idPool = new IdentityPool(stack, 'TestIdentityPoolUserPools', { + authenticationProviders: { + userPools: [new UserPoolAuthenticationProvider({ userPool: pool })], + }, + }); + idPool.addUserPoolAuthentication( + new UserPoolAuthenticationProvider({ + userPool: otherPool, + disableServerSideTokenCheck: true, + }), + ); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::Cognito::UserPool', 2); + temp.resourceCountIs('AWS::Cognito::UserPoolClient', 2); + temp.hasResourceProperties('AWS::Cognito::UserPoolClient', { + UserPoolId: { + Ref: 'PoolD3F588B8', + }, + AllowedOAuthFlows: [ + 'implicit', + 'code', + ], + AllowedOAuthFlowsUserPoolClient: true, + AllowedOAuthScopes: [ + 'profile', + 'phone', + 'email', + 'openid', + 'aws.cognito.signin.user.admin', + ], + CallbackURLs: [ + 'https://example.com', + ], + SupportedIdentityProviders: [ + 'poolProvider', + 'COGNITO', + ], + }); + temp.hasResourceProperties('AWS::Cognito::UserPoolClient', { + UserPoolId: { + Ref: 'OtherPool7DA7F2F7', + }, + AllowedOAuthFlows: [ + 'implicit', + 'code', + ], + AllowedOAuthFlowsUserPoolClient: true, + AllowedOAuthScopes: [ + 'profile', + 'phone', + 'email', + 'openid', + 'aws.cognito.signin.user.admin', + ], + CallbackURLs: [ + 'https://example.com', + ], + SupportedIdentityProviders: [ + 'otherPoolProvider', + 'COGNITO', + ], + }); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: false, + CognitoIdentityProviders: [ + { + ClientId: { + Ref: 'PoolUserPoolAuthenticationProviderClient20F2FFC4', + }, + ProviderName: { + 'Fn::Join': [ + '', + [ + 'cognito-idp.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'PoolD3F588B8', + }, + ], + ], + }, + ServerSideTokenCheck: true, + }, + { + ClientId: { + Ref: 'OtherPoolUserPoolAuthenticationProviderClient08F670F8', + }, + ProviderName: { + 'Fn::Join': [ + '', + [ + 'cognito-idp.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'OtherPool7DA7F2F7', + }, + ], + ], + }, + ServerSideTokenCheck: false, + }, + ], + }); + }); + + test('openId, saml, classicFlow, customProviders', () => { + const stack = new Stack(); + const openId = new OpenIdConnectProvider(stack, 'OpenId', { + url: 'https://example.com', + clientIds: ['client1', 'client2'], + thumbprints: ['thumbprint'], + }); + const saml = new SamlProvider(stack, 'Provider', { + metadataDocument: SamlMetadataDocument.fromXml('document'), + }); + new IdentityPool(stack, 'TestIdentityPoolCustomProviders', { + authenticationProviders: { + openIdConnectProviders: [openId], + samlProviders: [saml], + customProvider: 'my-custom-provider.com', + }, + allowClassicFlow: true, + }); + const temp = Template.fromStack(stack); + temp.resourceCountIs('Custom::AWSCDKOpenIdConnectProvider', 1); + temp.resourceCountIs('AWS::IAM::SAMLProvider', 1); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + AllowUnauthenticatedIdentities: false, + AllowClassicFlow: true, + DeveloperProviderName: 'my-custom-provider.com', + OpenIdConnectProviderARNs: [ + { + Ref: 'OpenId76D94D20', + }, + ], + SamlProviderARNs: [ + { + Ref: 'Provider2281708E', + }, + ], + }); + }); + + test('cognito authentication providers', () => { + const stack = new Stack(); + new IdentityPool(stack, 'TestIdentityPoolauthproviders', { + identityPoolName: 'my-id-pool', + authenticationProviders: { + amazon: { appId: 'amzn1.application.12312k3j234j13rjiwuenf' }, + google: { clientId: '12345678012.apps.googleusercontent.com' }, + }, + }); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::IAM::Role', 2); + temp.hasResourceProperties('AWS::Cognito::IdentityPool', { + IdentityPoolName: 'my-id-pool', + SupportedLoginProviders: { + 'www.amazon.com': 'amzn1.application.12312k3j234j13rjiwuenf', + 'accounts.google.com': '12345678012.apps.googleusercontent.com', + }, + }); + }); +}); + +describe('role mappings', () => { + test('using token', () => { + const stack = new Stack(); + new IdentityPool(stack, 'TestIdentityPoolRoleMappingToken', { + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + useToken: true, + }], + }); + Template.fromStack(stack).hasResourceProperties('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: 'TestIdentityPoolRoleMappingToken0B11D690', + }, + RoleMappings: { + 'www.amazon.com': { + AmbiguousRoleResolution: 'Deny', + IdentityProvider: 'www.amazon.com', + Type: 'Token', + }, + }, + Roles: { + authenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingTokenAuthenticatedRoleD99CE043', + 'Arn', + ], + }, + unauthenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingTokenUnauthenticatedRole1D86D800', + 'Arn', + ], + }, + }, + }); + }); + + test('rules type without rules throws', () => { + const stack = new Stack(); + expect(() => new IdentityPool(stack, 'TestIdentityPoolRoleMappingErrors', { + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + }], + })).toThrowError('IdentityPoolRoleMapping.rules is required when useToken is false'); + }); + + test('role mapping with rules configuration', () => { + const stack = new Stack(); + const adminRole = new Role(stack, 'adminRole', { + assumedBy: new ServicePrincipal('admin.amazonaws.com'), + }); + const nonAdminRole = new Role(stack, 'nonAdminRole', { + assumedBy: new AnyPrincipal(), + inlinePolicies: { + DenyAll: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.DENY, + actions: ['update:*', 'put:*', 'delete:*'], + resources: ['*'], + }), + ], + }), + }, + }); + const facebookRole = new Role(stack, 'facebookRole', { + assumedBy: new ArnPrincipal('arn:aws:iam::123456789012:user/FacebookUser'), + }); + const customRole = new Role(stack, 'customRole', { + assumedBy: new ArnPrincipal('arn:aws:iam::123456789012:user/CustomUser'), + }); + const idPool = new IdentityPool(stack, 'TestIdentityPoolRoleMappingRules', { + roleMappings: [{ + providerUrl: IdentityPoolProviderUrl.AMAZON, + resolveAmbiguousRoles: true, + rules: [ + { + claim: 'custom:admin', + claimValue: 'admin', + mappedRole: adminRole, + }, + { + claim: 'custom:admin', + claimValue: 'admin', + matchType: RoleMappingMatchType.NOTEQUAL, + mappedRole: nonAdminRole, + }, + ], + }], + }); + idPool.addRoleMappings({ + providerUrl: IdentityPoolProviderUrl.FACEBOOK, + rules: [ + { + claim: 'iss', + claimValue: 'https://graph.facebook.com', + mappedRole: facebookRole, + }, + ], + }, + { + providerUrl: IdentityPoolProviderUrl.custom('example.com'), + rules: [ + { + claim: 'iss', + claimValue: 'https://example.com', + mappedRole: customRole, + }, + ], + }); + const temp = Template.fromStack(stack); + temp.resourceCountIs('AWS::Cognito::IdentityPoolRoleAttachment', 2); + temp.hasResourceProperties('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: 'TestIdentityPoolRoleMappingRulesC8C07BC3', + }, + RoleMappings: { + 'www.amazon.com': { + AmbiguousRoleResolution: 'AuthenticatedRole', + IdentityProvider: 'www.amazon.com', + RulesConfiguration: { + Rules: [ + { + Claim: 'custom:admin', + MatchType: 'Equals', + RoleARN: { + 'Fn::GetAtt': [ + 'adminRoleC345D70B', + 'Arn', + ], + }, + Value: 'admin', + }, + { + Claim: 'custom:admin', + MatchType: 'NotEqual', + RoleARN: { + 'Fn::GetAtt': [ + 'nonAdminRole43C19D5C', + 'Arn', + ], + }, + Value: 'admin', + }, + ], + }, + Type: 'Rules', + }, + }, + Roles: { + authenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingRulesAuthenticatedRole14D102C7', + 'Arn', + ], + }, + unauthenticated: { + 'Fn::GetAtt': [ + 'TestIdentityPoolRoleMappingRulesUnauthenticatedRole79A7AF99', + 'Arn', + ], + }, + }, + }); + temp.hasResourceProperties('AWS::Cognito::IdentityPoolRoleAttachment', { + IdentityPoolId: { + Ref: 'TestIdentityPoolRoleMappingRulesC8C07BC3', + }, + RoleMappings: { + 'graph.facebook.com': { + AmbiguousRoleResolution: 'Deny', + IdentityProvider: 'graph.facebook.com', + RulesConfiguration: { + Rules: [ + { + Claim: 'iss', + MatchType: 'Equals', + RoleARN: { + 'Fn::GetAtt': [ + 'facebookRole9D649CD8', + 'Arn', + ], + }, + Value: 'https://graph.facebook.com', + }, + ], + }, + Type: 'Rules', + }, + 'example.com': { + AmbiguousRoleResolution: 'Deny', + IdentityProvider: 'example.com', + RulesConfiguration: { + Rules: [ + { + Claim: 'iss', + MatchType: 'Equals', + RoleARN: { + 'Fn::GetAtt': [ + 'customRole4C920FF0', + 'Arn', + ], + }, + Value: 'https://example.com', + }, + ], + }, + Type: 'Rules', + }, + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json new file mode 100644 index 0000000000000..b555de31baa1e --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json @@ -0,0 +1,417 @@ +{ + "Resources": { + "PoolD3F588B8": { + "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 {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "PoolUserPoolAuthenticationProviderClient20F2FFC4": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "PoolD3F588B8" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "PoolProviderGoogle76A1E8D0" + }, + "COGNITO" + ] + } + }, + "PoolProviderGoogle76A1E8D0": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "Google", + "ProviderType": "Google", + "UserPoolId": { + "Ref": "PoolD3F588B8" + }, + "AttributeMapping": { + "given_name": "given_name", + "family_name": "family_name", + "email": "email", + "gender": "gender", + "names": "names" + }, + "ProviderDetails": { + "client_id": "google-client-id", + "client_secret": "google-client-secret", + "authorize_scopes": "profile" + } + } + }, + "OtherPool7DA7F2F7": { + "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 {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "OtherPoolUserPoolAuthenticationProviderClient08F670F8": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "OtherPool7DA7F2F7" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "OtherPoolProviderAmazon4EB0592F" + }, + "COGNITO" + ] + } + }, + "OtherPoolProviderAmazon4EB0592F": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "LoginWithAmazon", + "ProviderType": "LoginWithAmazon", + "UserPoolId": { + "Ref": "OtherPool7DA7F2F7" + }, + "AttributeMapping": { + "given_name": "name", + "email": "email", + "userId": "user_id" + }, + "ProviderDetails": { + "client_id": "amzn-client-id", + "client_secret": "amzn-client-secret", + "authorize_scopes": "profile" + } + } + }, + "identitypoolE2A6D099": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "AllowClassicFlow": true, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "PoolUserPoolAuthenticationProviderClient20F2FFC4" + }, + "ProviderName": { + "Fn::Join": [ + "", + [ + "cognito-idp.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "PoolD3F588B8" + } + ] + ] + }, + "ServerSideTokenCheck": true + }, + { + "ClientId": { + "Ref": "OtherPoolUserPoolAuthenticationProviderClient08F670F8" + }, + "ProviderName": { + "Fn::Join": [ + "", + [ + "cognito-idp.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "OtherPool7DA7F2F7" + } + ] + ] + }, + "ServerSideTokenCheck": true + } + ], + "IdentityPoolName": "my-id-pool", + "SupportedLoginProviders": { + "www.amazon.com": "amzn1.application.12312k3j234j13rjiwuenf", + "accounts.google.com": "12345678012.apps.googleusercontent.com" + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolAuthenticatedRoleB074B49D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "identitypoolE2A6D099" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": { + "Fn::Join": [ + "", + [ + "Default Authenticated Role for Identity Pool ", + { + "Fn::GetAtt": [ + "identitypoolE2A6D099", + "Name" + ] + } + ] + ] + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolAuthenticatedRoleDefaultPolicyCB4D2992": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "identitypoolAuthenticatedRoleDefaultPolicyCB4D2992", + "Roles": [ + { + "Ref": "identitypoolAuthenticatedRoleB074B49D" + } + ] + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolUnauthenticatedRoleE61CAC70": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "identitypoolE2A6D099" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "unauthenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Description": { + "Fn::Join": [ + "", + [ + "Default Unauthenticated Role for Identity Pool ", + { + "Fn::GetAtt": [ + "identitypoolE2A6D099", + "Name" + ] + } + ] + ] + } + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolUnauthenticatedRoleDefaultPolicyBFACCE98": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:Get*", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "identitypoolUnauthenticatedRoleDefaultPolicyBFACCE98", + "Roles": [ + { + "Ref": "identitypoolUnauthenticatedRoleE61CAC70" + } + ] + }, + "DependsOn": [ + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + }, + "identitypoolDefaultRoleAttachment6BCAB114": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "identitypoolE2A6D099" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "identitypoolAuthenticatedRoleB074B49D", + "Arn" + ] + }, + "unauthenticated": { + "Fn::GetAtt": [ + "identitypoolUnauthenticatedRoleE61CAC70", + "Arn" + ] + } + } + }, + "DependsOn": [ + "identitypoolAuthenticatedRoleDefaultPolicyCB4D2992", + "identitypoolAuthenticatedRoleB074B49D", + "identitypoolE2A6D099", + "identitypoolUnauthenticatedRoleDefaultPolicyBFACCE98", + "identitypoolUnauthenticatedRoleE61CAC70", + "OtherPool7DA7F2F7", + "OtherPoolUserPoolAuthenticationProviderClient08F670F8", + "PoolD3F588B8", + "PoolUserPoolAuthenticationProviderClient20F2FFC4" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts new file mode 100644 index 0000000000000..5fc7ee1027084 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.ts @@ -0,0 +1,73 @@ +import { + UserPool, + UserPoolIdentityProviderGoogle, + UserPoolIdentityProviderAmazon, + ProviderAttribute, +} from '@aws-cdk/aws-cognito'; +import { + Effect, + PolicyStatement, +} from '@aws-cdk/aws-iam'; +import { + App, + Stack, +} from '@aws-cdk/core'; +import { + IdentityPool, +} from '../lib/identitypool'; +import { + UserPoolAuthenticationProvider, +} from '../lib/identitypool-user-pool-authentication-provider'; + +const app = new App(); +const stack = new Stack(app, 'integ-identitypool'); + +const userPool = new UserPool(stack, 'Pool'); +new UserPoolIdentityProviderGoogle(stack, 'PoolProviderGoogle', { + userPool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + attributeMapping: { + givenName: ProviderAttribute.GOOGLE_GIVEN_NAME, + familyName: ProviderAttribute.GOOGLE_FAMILY_NAME, + email: ProviderAttribute.GOOGLE_EMAIL, + gender: ProviderAttribute.GOOGLE_GENDER, + custom: { + names: ProviderAttribute.GOOGLE_NAMES, + }, + }, +}); +const otherPool = new UserPool(stack, 'OtherPool'); +new UserPoolIdentityProviderAmazon(stack, 'OtherPoolProviderAmazon', { + userPool: otherPool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + email: ProviderAttribute.AMAZON_EMAIL, + custom: { + userId: ProviderAttribute.AMAZON_USER_ID, + }, + }, +}); +const idPool = new IdentityPool(stack, 'identitypool', { + authenticationProviders: { + userPools: [new UserPoolAuthenticationProvider({ userPool })], + amazon: { appId: 'amzn1.application.12312k3j234j13rjiwuenf' }, + google: { clientId: '12345678012.apps.googleusercontent.com' }, + }, + allowClassicFlow: true, + identityPoolName: 'my-id-pool', +}); +idPool.authenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:*'], + resources: ['*'], +})); +idPool.unauthenticatedRole.addToPrincipalPolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:Get*'], + resources: ['*'], +})); +idPool.addUserPoolAuthentication(new UserPoolAuthenticationProvider({ userPool: otherPool })); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 2cb86ba2885dd..ef968f5151bd2 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -31,7 +31,7 @@ The two main components of Amazon Cognito are [user pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) and [identity pools](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html). User pools are user directories that provide sign-up and sign-in options for your app users. Identity pools enable you to grant your users access to -other AWS services. +other AWS services. Identity Pool L2 Constructs can be found [here](../aws-cognito-identitypool). This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index cbc5ff8b2529b..f37d046fa5448 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -171,6 +171,7 @@ "@aws-cdk/aws-codestarconnections": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-cognito-identitypool": "0.0.0", "@aws-cdk/aws-config": "0.0.0", "@aws-cdk/aws-connect": "0.0.0", "@aws-cdk/aws-cur": "0.0.0", @@ -428,6 +429,7 @@ "./aws-codestarconnections": "./aws-codestarconnections/index.js", "./aws-codestarnotifications": "./aws-codestarnotifications/index.js", "./aws-cognito": "./aws-cognito/index.js", + "./aws-cognito-identitypool": "./aws-cognito-identitypool/index.js", "./aws-config": "./aws-config/index.js", "./aws-connect": "./aws-connect/index.js", "./aws-cur": "./aws-cur/index.js", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 7e09da6df887f..0d2fbe411cd90 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -83,6 +83,7 @@ "@aws-cdk/aws-codestarconnections": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-cognito-identitypool": "0.0.0", "@aws-cdk/aws-config": "0.0.0", "@aws-cdk/aws-connect": "0.0.0", "@aws-cdk/aws-cur": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index 4c65c9867572e..a13001e6c55b3 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -168,6 +168,7 @@ "@aws-cdk/aws-codestarconnections": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-cognito-identitypool": "0.0.0", "@aws-cdk/aws-config": "0.0.0", "@aws-cdk/aws-connect": "0.0.0", "@aws-cdk/aws-cur": "0.0.0", diff --git a/yarn.lock b/yarn.lock index a8a93353b579b..bf3c0213efaee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1694,7 +1694,7 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/jest@^27.4.0": +"@types/jest@^27.0.3", "@types/jest@^27.4.0": version "27.4.0" resolved "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed" integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== @@ -5802,7 +5802,7 @@ jest-worker@^27.4.6: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^27.3.1, jest@^27.4.7: +jest@^27.3.1, jest@^27.4.5, jest@^27.4.7: version "27.4.7" resolved "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz#87f74b9026a1592f2da05b4d258e57505f28eca4" integrity sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg==