diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index ac6e0510c3dc5..6c85ce75396ce 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -341,13 +341,61 @@ Client](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-sett The following code creates an app client and retrieves the client id - ```ts -const pool = new UserPool(this, 'Pool'); +const pool = new UserPool(this, 'pool'); +const client = pool.addClient('customer-app-client'); +const clientId = client.userPoolClientId; +``` + +Existing app clients can be imported into the CDK app using the `UserPoolClient.fromUserPoolClientId()` API. For new +and imported user pools, clients can also be created via the `UserPoolClient` constructor, as so - -const client = new UserPoolClient(stack, 'Client', { - userPool: pool +```ts +const importedPool = UserPool.fromUserPoolId(this, 'imported-pool', 'us-east-1_oiuR12Abd'); +new UserPoolClient(this, 'customer-app-client', { + userPool: importedPool }); +``` -const clientId = client.userPoolClientId; +Clients can be configured with authentication flows. Authentication flows allow users on a client to be authenticated +with a user pool. Cognito user pools provide several several different types of authentication, such as, SRP (Secure +Remote Password) authentication, username-and-password authentication, etc. Learn more about this at [UserPool Authentication +Flow](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html). + +The following code configures a client to use both SRP and username-and-password authentication - + +```ts +const pool = new UserPool(this, 'pool'); +pool.addClient('app-client', { + authFlows: { + userPassword: true, + userSrp: true, + } +}); ``` -Existing app clients can be imported into the CDK app using the `UserPoolClient.fromUserPoolClientId()` API. +Custom authentication protocols can be configured by setting the `custom` property under `authFlow` and defining lambda +functions for the corresponding user pool [triggers](#lambda-triggers). Learn more at [Custom Authentication +Flow](https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html#amazon-cognito-user-pools-custom-authentication-flow). + +In addition to these authentication mechanisms, Cognito user pools also support using OAuth 2.0 framework for +authenticating users. User pool clients can be configured with OAuth 2.0 authorization flows and scopes. Learn more +about the [OAuth 2.0 authorization framework](https://tools.ietf.org/html/rfc6749) and [Cognito user pool's +implementation of +OAuth2.0](https://aws.amazon.com/blogs/mobile/understanding-amazon-cognito-user-pool-oauth-2-0-grants/). + +The following code configures an app client with the authorization code grant flow and registers the the app's welcome +page as a callback (or redirect) URL. It also configures the access token scope to 'openid'. All of these concepts can +be found in the [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749). + +```ts +const pool = new UserPool(this, 'Pool'); +pool.addClient('app-client', { + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + scopes: [ OAuthScope.OPENID ], + callbackUrls: [ 'https://my-app-domain.com/welcome' ], + } +}); +``` diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index c8b11ab7abc19..639de30001655 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -4,39 +4,157 @@ import { IUserPool } from './user-pool'; /** * Types of authentication flow + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html */ -export enum AuthFlow { +export interface AuthFlow { /** - * Enable flow for server-side or admin authentication (no client app) + * Enable admin based user password authentication flow + * @default false */ - ADMIN_NO_SRP = 'ADMIN_NO_SRP_AUTH', + readonly adminUserPassword?: boolean; /** * Enable custom authentication flow + * @default false */ - CUSTOM_FLOW_ONLY = 'CUSTOM_AUTH_FLOW_ONLY', + readonly custom?: boolean; /** * Enable auth using username & password + * @default false + */ + readonly userPassword?: boolean; + + /** + * Enable SRP based authentication + * @default false */ - USER_PASSWORD = 'USER_PASSWORD_AUTH' + readonly userSrp?: boolean; + + /** + * Enable authflow to refresh tokens + * @default false + */ + readonly refreshToken?: boolean; +} + +/** + * OAuth settings to configure the interaction between the app and this client. + */ +export interface OAuthSettings { + + /** + * OAuth flows that are allowed with this client. + * @see - the 'Allowed OAuth Flows' section at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html + * @default - all OAuth flows disabled + */ + readonly flows: OAuthFlows; + + /** + * List of allowed redirect URLs for the identity providers. + * @default - no callback URLs + */ + readonly callbackUrls?: string[]; + + /** + * OAuth scopes that are allowed with this client. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html + * @default - no OAuth scopes are configured. + */ + readonly scopes: OAuthScope[]; +} + +/** + * Types of OAuth grant flows + * @see - the 'Allowed OAuth Flows' section at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html + */ +export interface OAuthFlows { + /** + * Initiate an authorization code grant flow, which provides an authorization code as the response. + * @default false + */ + readonly authorizationCodeGrant?: boolean; + + /** + * The client should get the access token and ID token directly. + * @default false + */ + readonly implicitCodeGrant?: boolean; + + /** + * Client should get the access token and ID token from the token endpoint + * using a combination of client and client_secret. + * @default false + */ + readonly clientCredentials?: boolean; +} + +/** + * OAuth scopes that are allowed with this client. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html + */ +export class OAuthScope { + /** + * Grants access to the 'phone_number' and 'phone_number_verified' claims. + * Automatically includes access to `OAuthScope.OPENID`. + */ + public static readonly PHONE = new OAuthScope('phone'); + + /** + * Grants access to the 'email' and 'email_verified' claims. + * Automatically includes access to `OAuthScope.OPENID`. + */ + public static readonly EMAIL = new OAuthScope('email'); + + /** + * Returns all user attributes in the ID token that are readable by the client + */ + public static readonly OPENID = new OAuthScope('openid'); + + /** + * Grants access to all user attributes that are readable by the client + * Automatically includes access to `OAuthScope.OPENID`. + */ + public static readonly PROFILE = new OAuthScope('profile'); + + /** + * Grants access to Amazon Cognito User Pool API operations that require access tokens, + * such as UpdateUserAttributes and VerifyUserAttribute. + */ + public static readonly COGNITO_ADMIN = new OAuthScope('aws.cognito.signin.user.admin'); + + /** + * Custom scope is one that you define for your own resource server in the Resource Servers. + * The format is 'resource-server-identifier/scope'. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html + */ + public static custom(name: string) { + return new OAuthScope(name); + } + + // tslint:disable:max-line-length + /** + * The name of this scope as recognized by CloudFormation. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolclient.html#cfn-cognito-userpoolclient-allowedoauthscopes + */ + // tslint:enable:max-line-length + public readonly scopeName: string; + + private constructor(scopeName: string) { + this.scopeName = scopeName; + } } /** * Properties for the UserPoolClient construct */ -export interface UserPoolClientProps { +export interface UserPoolClientOptions { /** * Name of the application client * @default - cloudformation generated name */ readonly userPoolClientName?: string; - /** - * The UserPool resource this client will have access to - */ - readonly userPool: IUserPool; - /** * Whether to generate a client secret * @default false @@ -44,10 +162,27 @@ export interface UserPoolClientProps { readonly generateSecret?: boolean; /** - * List of enabled authentication flows - * @default - no enabled flows + * The set of OAuth authentication flows to enable on the client + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html + * @default - all auth flows disabled + */ + readonly authFlows?: AuthFlow; + + /** + * OAuth settings for this to client to interact with the app. + * @default - see defaults in `OAuthSettings` + */ + readonly oAuth?: OAuthSettings; +} + +/** + * Properties for the UserPoolClient construct + */ +export interface UserPoolClientProps extends UserPoolClientOptions { + /** + * The UserPool resource this client will have access to */ - readonly enabledAuthFlows?: AuthFlow[] + readonly userPool: IUserPool; } /** @@ -94,7 +229,11 @@ export class UserPoolClient extends Resource implements IUserPoolClient { clientName: props.userPoolClientName, generateSecret: props.generateSecret, userPoolId: props.userPool.userPoolId, - explicitAuthFlows: props.enabledAuthFlows + explicitAuthFlows: this.configureAuthFlows(props), + allowedOAuthFlows: this.configureOAuthFlows(props.oAuth), + allowedOAuthScopes: this.configureOAuthScopes(props.oAuth), + callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined, + allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, }); this.userPoolClientId = resource.ref; @@ -111,4 +250,51 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } return this._userPoolClientName; } + + private configureAuthFlows(props: UserPoolClientProps): string[] | undefined { + const authFlows: string[] = []; + if (props.authFlows?.userPassword) { authFlows.push('ALLOW_USER_PASSWORD_AUTH'); } + if (props.authFlows?.adminUserPassword) { authFlows.push('ALLOW_ADMIN_USER_PASSWORD_AUTH'); } + if (props.authFlows?.custom) { authFlows.push('ALLOW_CUSTOM_AUTH'); } + if (props.authFlows?.userSrp) { authFlows.push('ALLOW_USER_SRP_AUTH'); } + if (props.authFlows?.refreshToken) { authFlows.push('ALLOW_REFRESH_TOKEN_AUTH'); } + + if (authFlows.length === 0) { + return undefined; + } + return authFlows; + } + + private configureOAuthFlows(oAuth?: OAuthSettings): string[] | undefined { + if (oAuth?.flows.authorizationCodeGrant || oAuth?.flows.implicitCodeGrant) { + if (oAuth?.callbackUrls === undefined || oAuth?.callbackUrls.length === 0) { + throw new Error('callbackUrl must be specified when codeGrant or implicitGrant OAuth flows are enabled.'); + } + if (oAuth?.flows.clientCredentials) { + throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); + } + } + + const oAuthFlows: string[] = []; + if (oAuth?.flows.clientCredentials) { oAuthFlows.push('client_credentials'); } + if (oAuth?.flows.implicitCodeGrant) { oAuthFlows.push('implicit'); } + if (oAuth?.flows.authorizationCodeGrant) { oAuthFlows.push('code'); } + + if (oAuthFlows.length === 0) { + return undefined; + } + return oAuthFlows; + } + + private configureOAuthScopes(oAuth?: OAuthSettings): string[] | undefined { + const oAuthScopes = new Set(oAuth?.scopes.map((x) => x.scopeName)); + const autoOpenIdScopes = [ OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.PROFILE ]; + if (autoOpenIdScopes.reduce((agg, s) => agg || oAuthScopes.has(s.scopeName), false)) { + oAuthScopes.add(OAuthScope.OPENID.scopeName); + } + if (oAuthScopes.size > 0) { + return Array.from(oAuthScopes); + } + return undefined; + } } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 3c2b870d85d33..4185047182435 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -3,6 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; +import { IUserPoolClient, UserPoolClient, UserPoolClientOptions } from './user-pool-client'; /** * The different ways in which users of this pool can sign up or sign in. @@ -515,6 +516,11 @@ export interface IUserPool extends IResource { * @attribute */ readonly userPoolArn: string; + + /** + * Create a user pool client. + */ + addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient; } /** @@ -532,6 +538,13 @@ export class UserPool extends Resource implements IUserPool { resource: 'userpool', resourceName: userPoolId, }); + + public addClient(clientId: string, options?: UserPoolClientOptions): IUserPoolClient { + return new UserPoolClient(this, clientId, { + userPool: this, + ...options, + }); + } } return new Import(scope, id); } @@ -540,11 +553,7 @@ export class UserPool extends Resource implements IUserPool { * Import an existing user pool based on its ARN. */ public static fromUserPoolArn(scope: Construct, id: string, userPoolArn: string): IUserPool { - class Import extends Resource implements IUserPool { - public readonly userPoolArn = userPoolArn; - public readonly userPoolId = Stack.of(this).parseArn(userPoolArn).resourceName!; - } - return new Import(scope, id); + return UserPool.fromUserPoolId(scope, id, Stack.of(scope).parseArn(userPoolArn).resourceName!); } /** @@ -649,6 +658,13 @@ export class UserPool extends Resource implements IUserPool { (this.triggers as any)[operation.operationName] = fn.functionArn; } + public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { + return new UserPoolClient(this, id, { + userPool: this, + ...options + }); + } + private addLambdaPermission(fn: lambda.IFunction, name: string): void { const capitalize = name.charAt(0).toUpperCase() + name.slice(1); fn.addPermission(`${capitalize}Cognito`, { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explcit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explcit-props.expected.json new file mode 100644 index 0000000000000..f3da0535c775d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explcit-props.expected.json @@ -0,0 +1,100 @@ +{ + "Resources": { + "myuserpoolsmsRole0E16FDD9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpoolclientexplicitpropsmyuserpoolFC6541FF" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "myuserpool01998219": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "Hello {username}, Your verification code is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpoolclientexplicitpropsmyuserpoolFC6541FF", + "SnsCallerArn": { + "Fn::GetAtt": [ + "myuserpoolsmsRole0E16FDD9", + "Arn" + ] + } + }, + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "Hello {username}, Your verification code is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "myuserpoolmyuserpoolclientAFB2274E": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "myuserpool01998219" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "phone", + "email", + "openid", + "profile", + "aws.cognito.signin.user.admin", + "my-resource-server/my-scope" + ], + "CallbackURLs": [ + "https://redirect-here.myapp.com" + ], + "ClientName": "myuserpoolclient", + "ExplicitAuthFlows": [ + "ALLOW_USER_PASSWORD_AUTH", + "ALLOW_ADMIN_USER_PASSWORD_AUTH", + "ALLOW_CUSTOM_AUTH", + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ], + "GenerateSecret": true + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explcit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explcit-props.ts new file mode 100644 index 0000000000000..4870ab2276738 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explcit-props.ts @@ -0,0 +1,34 @@ +import { App, Stack } from '@aws-cdk/core'; +import { OAuthScope, UserPool } from '../lib'; + +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-client-explicit-props'); + +const userpool = new UserPool(stack, 'myuserpool'); + +userpool.addClient('myuserpoolclient', { + userPoolClientName: 'myuserpoolclient', + authFlows: { + adminUserPassword: true, + custom: true, + refreshToken: true, + userPassword: true, + userSrp: true, + }, + generateSecret: true, + oAuth: { + flows: { + implicitCodeGrant: true, + authorizationCodeGrant: true, + }, + scopes: [ + OAuthScope.PHONE, + OAuthScope.EMAIL, + OAuthScope.OPENID, + OAuthScope.PROFILE, + OAuthScope.COGNITO_ADMIN, + OAuthScope.custom('my-resource-server/my-scope'), + ], + callbackUrls: [ 'https://redirect-here.myapp.com' ], + }, +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 278ef92cc45a6..15ae1f90a3270 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -1,6 +1,7 @@ +import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import { UserPool, UserPoolClient } from '../lib'; +import { OAuthScope, UserPool, UserPoolClient } from '../lib'; describe('User Pool Client', () => { test('default setup', () => { @@ -48,4 +49,238 @@ describe('User Pool Client', () => { // THEN expect(client.userPoolClientId).toEqual('client-id-1'); }); + + test('ExplicitAuthFlows is absent by default', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client'); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ExplicitAuthFlows: ABSENT, + }); + }); + + test('ExplicitAuthFlows are correctly named', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client', { + authFlows: { + adminUserPassword: true, + custom: true, + refreshToken: true, + userPassword: true, + userSrp: true, + } + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ExplicitAuthFlows: [ + 'ALLOW_USER_PASSWORD_AUTH', + 'ALLOW_ADMIN_USER_PASSWORD_AUTH', + 'ALLOW_CUSTOM_AUTH', + 'ALLOW_USER_SRP_AUTH', + 'ALLOW_REFRESH_TOKEN_AUTH', + ], + }); + }); + + test('AllowedOAuthFlows is absent by default', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client'); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: ABSENT, + // AllowedOAuthFlowsUserPoolClient: ABSENT, + }); + }); + + test('AllowedOAuthFlows are correctly named', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client1', { + oAuth: { + flows: { + authorizationCodeGrant: true, + implicitCodeGrant: true, + }, + callbackUrls: [ 'redirect-url' ], + scopes: [ OAuthScope.PHONE ], + }, + }); + pool.addClient('Client2', { + oAuth: { + flows: { + clientCredentials: true, + }, + callbackUrls: [ 'redirect-url' ], + scopes: [ OAuthScope.PHONE ], + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'implicit', 'code' ], + AllowedOAuthFlowsUserPoolClient: true, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'client_credentials' ], + AllowedOAuthFlowsUserPoolClient: true, + }); + }); + + test('fails when callbackUrls are not specified for codeGrant or implicitGrant', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + expect(() => pool.addClient('Client1', { + oAuth: { + flows: { authorizationCodeGrant: true }, + scopes: [ OAuthScope.PHONE ], + } + })).toThrow(/callbackUrl must be specified/); + + expect(() => pool.addClient('Client2', { + oAuth: { + flows: { implicitCodeGrant: true }, + scopes: [ OAuthScope.PHONE ], + }, + })).toThrow(/callbackUrl must be specified/); + + expect(() => pool.addClient('Client3', { + oAuth: { + flows: { clientCredentials: true }, + scopes: [ OAuthScope.PHONE ], + } + })).not.toThrow(); + }); + + test('fails when clientCredentials OAuth flow is selected along with codeGrant or implicitGrant', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + expect(() => pool.addClient('Client1', { + oAuth: { + flows: { + authorizationCodeGrant: true, + clientCredentials: true, + }, + callbackUrls: [ 'redirect-url' ], + scopes: [ OAuthScope.PHONE ], + }, + })).toThrow(/clientCredentials OAuth flow cannot be selected/); + + expect(() => pool.addClient('Client2', { + oAuth: { + flows: { + implicitCodeGrant: true, + clientCredentials: true, + }, + callbackUrls: [ 'redirect-url' ], + scopes: [ OAuthScope.PHONE ], + }, + })).toThrow(/clientCredentials OAuth flow cannot be selected/); + }); + + test('OAuth scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client', { + oAuth: { + flows: { clientCredentials: true, }, + scopes: [ + OAuthScope.PHONE, + OAuthScope.EMAIL, + OAuthScope.OPENID, + OAuthScope.PROFILE, + OAuthScope.COGNITO_ADMIN, + OAuthScope.custom('my-resource-server/my-own-scope'), + ], + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthScopes: [ + 'phone', + 'email', + 'openid', + 'profile', + 'aws.cognito.signin.user.admin', + 'my-resource-server/my-own-scope' + ], + }); + }); + + test('OAuthScope - openid is included when email or phone is specified', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('Client1', { + userPoolClientName: 'Client1', + oAuth: { + flows: { clientCredentials: true, }, + scopes: [ OAuthScope.PHONE, ], + }, + }); + pool.addClient('Client2', { + userPoolClientName: 'Client2', + oAuth: { + flows: { clientCredentials: true, }, + scopes: [ OAuthScope.EMAIL, ], + }, + }); + pool.addClient('Client3', { + userPoolClientName: 'Client3', + oAuth: { + flows: { clientCredentials: true, }, + scopes: [ OAuthScope.PROFILE, ], + }, + }); + pool.addClient('Client4', { + userPoolClientName: 'Client4', + oAuth: { + flows: { clientCredentials: true, }, + scopes: [ OAuthScope.COGNITO_ADMIN, ], + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client1', + AllowedOAuthScopes: [ 'phone', 'openid', ], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client2', + AllowedOAuthScopes: [ 'email', 'openid', ], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client3', + AllowedOAuthScopes: [ 'profile', 'openid', ], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'Client4', + AllowedOAuthScopes: [ 'aws.cognito.signin.user.admin' ], + }); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 696ec8dfc5f80..7a997b5bc3554 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -227,7 +227,17 @@ describe('User Pool', () => { // WHEN const pool = UserPool.fromUserPoolArn(stack, 'userpool', userPoolArn); expect(pool.userPoolId).toEqual('test-user-pool'); - expect(pool.userPoolArn).toEqual(userPoolArn); + expect(stack.resolve(pool.userPoolArn)).toEqual({ + 'Fn::Join': [ '', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':cognito-idp:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':userpool/test-user-pool' + ] ] + }); }); test('support tags', () => { @@ -753,6 +763,31 @@ describe('User Pool', () => { }); }); +test('addClient', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const userpool = new UserPool(stack, 'Pool'); + userpool.addClient('UserPoolClient', { + userPoolClientName: 'userpoolclient' + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addClient('UserPoolImportedClient', { + userPoolClientName: 'userpoolimportedclient' + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'userpoolclient', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + ClientName: 'userpoolimportedclient', + UserPoolId: stack.resolve(imported.userPoolId), + }); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name,