Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cognito): import an existing user pool client #7091

Merged
merged 4 commits into from
Mar 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw
- [Emails](#emails)
- [Lambda Triggers](#lambda-triggers)
- [Import](#importing-user-pools)
- [App Clients](#app-clients)

## User Pools

Expand Down Expand Up @@ -328,4 +329,25 @@ const awesomePool = UserPool.fromUserPoolId(stack, 'awesome-user-pool', 'us-east

const otherAwesomePool = UserPool.fromUserPoolArn(stack, 'other-awesome-user-pool',
'arn:aws:cognito-idp:eu-west-1:123456789012:userpool/us-east-1_mtRyYQ14D');
```
```

### App Clients

An app is an entity within a user pool that has permission to call unauthenticated APIs (APIs that do not have an
authenticated user), such as APIs to register, sign in, and handle forgotten passwords. To call these APIs, you need an
app client ID and an optional client secret. Read [Configuring a User Pool App
Client](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html) to learn more.

The following code creates an app client and retrieves the client id -

```ts
const pool = new UserPool(this, 'Pool');

const client = new UserPoolClient(stack, 'Client', {
userPool: pool
});

const clientId = client.userPoolClientId;
```

Existing app clients can be imported into the CDK app using the `UserPoolClient.fromUserPoolClientId()` API.
63 changes: 46 additions & 17 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Construct, Resource } from '@aws-cdk/core';
import { Construct, IResource, Resource } from '@aws-cdk/core';
import { CfnUserPoolClient } from './cognito.generated';
import { IUserPool } from './user-pool';

Expand All @@ -22,10 +22,13 @@ export enum AuthFlow {
USER_PASSWORD = 'USER_PASSWORD_AUTH'
}

/**
* Properties for the UserPoolClient construct
*/
export interface UserPoolClientProps {
/**
* Name of the application client
* @default cloudformation generated name
* @default - cloudformation generated name
*/
readonly userPoolClientName?: string;

Expand All @@ -42,44 +45,70 @@ export interface UserPoolClientProps {

/**
* List of enabled authentication flows
* @default no enabled flows
* @default - no enabled flows
*/
readonly enabledAuthFlows?: AuthFlow[]
}

/**
* Define a UserPool App Client
* Represents a Cognito user pool client.
*/
export class UserPoolClient extends Resource {
export interface IUserPoolClient extends IResource {
/**
* Name of the application client
* @attribute
*/
public readonly userPoolClientId: string;
readonly userPoolClientId: string;
}

/**
* Define a UserPool App Client
*/
export class UserPoolClient extends Resource implements IUserPoolClient {
/**
* @attribute
* Import a user pool client given its id.
*/
public readonly userPoolClientName: string;
public static fromUserPoolClientId(scope: Construct, id: string, userPoolClientId: string): IUserPoolClient {
class Import extends Resource implements IUserPoolClient {
public readonly userPoolClientId = userPoolClientId;
}

/**
* @attribute
return new Import(scope, id);
}

public readonly userPoolClientId: string;
nija-at marked this conversation as resolved.
Show resolved Hide resolved
private readonly _userPoolClientName?: string;

/*
* Note to implementers: Two CloudFormation return values Name and ClientSecret are part of the spec.
* However, they have been explicity not implemented here. They are not documented in CloudFormation, and
* CloudFormation returns the following the string when these two attributes are 'GetAtt' - "attribute not supported
* at this time, please use the CLI or Console to retrieve this value".
* Awaiting updates from CloudFormation.
*/
public readonly userPoolClientClientSecret: string;

constructor(scope: Construct, id: string, props: UserPoolClientProps) {
super(scope, id, {
physicalName: props.userPoolClientName,
});
super(scope, id);
Comment on lines -70 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we not using physical name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User pool clients generate their own primary identifiers (e.g. 6tapehhff4ov922cjnheqpfr23) that cannot be specified as input. AFAIU, physical names are set for resources whose primary identifiers can be configured by the user. Am I wrong?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay makes sense. Maybe add a comment.


const resource = new CfnUserPoolClient(this, 'Resource', {
clientName: this.physicalName,
clientName: props.userPoolClientName,
generateSecret: props.generateSecret,
userPoolId: props.userPool.userPoolId,
explicitAuthFlows: props.enabledAuthFlows
});

this.userPoolClientId = resource.ref;
this.userPoolClientClientSecret = resource.attrClientSecret;
this.userPoolClientName = resource.attrName;
this._userPoolClientName = props.userPoolClientName;
}

/**
* The client name that was specified via the `userPoolClientName` property during initialization,
* throws an error otherwise.
*/
public get userPoolClientName(): string {
if (this._userPoolClientName === undefined) {
throw new Error('userPoolClientName is available only if specified on the UserPoolClient during initialization');
}
return this._userPoolClientName;
}
}
6 changes: 2 additions & 4 deletions packages/@aws-cdk/aws-cognito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@
},
"awslint": {
"exclude": [
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientClientSecret",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientId",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientName",
"docs-public-apis:@aws-cdk/aws-cognito.UserPoolClientProps"
"attribute-tag:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientName",
"resource-attribute:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientClientSecret"
Comment on lines +93 to +94
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

]
},
"stability": "experimental",
Expand Down
36 changes: 33 additions & 3 deletions packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,46 @@ describe('User Pool Client', () => {
test('default setup', () => {
// GIVEN
const stack = new Stack();
const pool = new UserPool(stack, 'Pool', { });
const pool = new UserPool(stack, 'Pool');

// WHEN
new UserPoolClient(stack, 'Client', {
userPool: pool
});

// THEN
expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', {
UserPoolId: stack.resolve(pool.userPoolId)
expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', {
UserPoolId: stack.resolve(pool.userPoolId),
});
});

test('client name', () => {
// GIVEN
const stack = new Stack();
const pool = new UserPool(stack, 'Pool');

// WHEN
const client1 = new UserPoolClient(stack, 'Client1', {
userPool: pool,
userPoolClientName: 'myclient'
});
const client2 = new UserPoolClient(stack, 'Client2', {
userPool: pool,
});

// THEN
expect(client1.userPoolClientName).toEqual('myclient');
expect(() => client2.userPoolClientName).toThrow(/available only if specified on the UserPoolClient during initialization/);
});

test('import', () => {
// GIVEN
const stack = new Stack();

// WHEN
const client = UserPoolClient.fromUserPoolClientId(stack, 'Client', 'client-id-1');

// THEN
expect(client.userPoolClientId).toEqual('client-id-1');
});
});