Skip to content

Commit

Permalink
feat(cognito): allow retrieval of UserPoolClient generated client sec…
Browse files Browse the repository at this point in the history
…ret (#21262)

Consumers of the `cognito.UserPoolClient` construct currently have a way to retrieve useful information and pass that information to other pieces of infrastructure (e.g. Lambdas, ECS, Parameter Store, CfnOutput), e.g. the Client ID, which is useful for certain operations against the Cognito user pool that require a Cognito client.

However, if consumers decide to configure the Client, with a pre-generated (random) Client Secret for security reasons, by passing the `generateSecret: true` prop to `cognito.UserPoolClient`, most operations to the Cognito UserPool now require the Client ID AND Client Secret, otherwise they generate the `Unable to verify secret hash for client <client-id>` error (see [AWS support article](https://aws.amazon.com/premiumsupport/knowledge-center/cognito-unable-to-verify-secret-hash/) for more info).

Currently, the construct exposes no way to retrieve the Client Secret, forcing consumers to work around this by using custom resources or custom AWS SDK calls to retrieve it.

This PR addresses that, by exposing a new getter method of the `UserPoolClient` class, allowing consumers to read it on demand and pass it to other pieces of infrastructure, using code like:

```ts
const userPoolClient = new cognito.UserPoolClient(this, 'UserPoolClient', {
  userPool,
  generateSecret: true,
});

// Allows you to pass the generated secret to other pieces of infrastructure
const secret = userPoolClient.userPoolClientSecret;
```

Closes #7225

----

### All Submissions:

* [X] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Dzhuneyt authored Aug 5, 2022
1 parent 0ff1231 commit 67a24ba
Show file tree
Hide file tree
Showing 15 changed files with 1,124 additions and 11 deletions.
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,20 @@ pool.addClient('app-client', {
});
```

User Pool clients can generate a client ID as well as a client secret, to support more advanced authentication workflows.

To create a client with an autogenerated client secret, pass the `generateSecret: true` prop:

```ts
const userPoolClient = new cognito.UserPoolClient(this, 'UserPoolClient', {
userPool: importedPool,
generateSecret: true,
});

// Allows you to pass the generated secret to other pieces of infrastructure
const secret = userPoolClient.userPoolClientSecret;
```

### Resource Servers

A resource server is a server for access-protected resources. It handles authenticated requests from an app that has an
Expand Down
56 changes: 55 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IResource, Resource, Duration } from '@aws-cdk/core';
import { IResource, Resource, Duration, Stack, SecretValue } from '@aws-cdk/core';
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources';
import { Construct } from 'constructs';
import { CfnUserPoolClient } from './cognito.generated';
import { IUserPool } from './user-pool';
Expand Down Expand Up @@ -321,24 +322,39 @@ export interface IUserPoolClient extends IResource {
* @attribute
*/
readonly userPoolClientId: string;

/**
* The generated client secret. Only available if the "generateSecret" props is set to true
* @attribute
*/
readonly userPoolClientSecret: SecretValue;
}

/**
* Define a UserPool App Client
*/
export class UserPoolClient extends Resource implements IUserPoolClient {

/**
* Import a user pool client given its id.
*/
public static fromUserPoolClientId(scope: Construct, id: string, userPoolClientId: string): IUserPoolClient {
class Import extends Resource implements IUserPoolClient {
public readonly userPoolClientId = userPoolClientId;
get userPoolClientSecret(): SecretValue {
throw new Error('UserPool Client Secret is not available for imported Clients');
}
}

return new Import(scope, id);
}

public readonly userPoolClientId: string;

private _generateSecret?: boolean;
private readonly userPool: IUserPool;
private _userPoolClientSecret?: SecretValue;

/**
* The OAuth flows enabled for this client.
*/
Expand Down Expand Up @@ -374,6 +390,9 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
}
}

this._generateSecret = props.generateSecret;
this.userPool = props.userPool;

const resource = new CfnUserPoolClient(this, 'Resource', {
clientName: props.userPoolClientName,
generateSecret: props.generateSecret,
Expand Down Expand Up @@ -407,6 +426,41 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
return this._userPoolClientName;
}

public get userPoolClientSecret(): SecretValue {
if (!this._generateSecret) {
throw new Error(
'userPoolClientSecret is available only if generateSecret is set to true.',
);
}

// Create the Custom Resource that assists in resolving the User Pool Client secret
// just once, no matter how many times this method is called
if (!this._userPoolClientSecret) {
this._userPoolClientSecret = SecretValue.resourceAttribute(new AwsCustomResource(
this,
'DescribeCognitoUserPoolClient',
{
resourceType: 'Custom::DescribeCognitoUserPoolClient',
onCreate: {
region: Stack.of(this).region,
service: 'CognitoIdentityServiceProvider',
action: 'describeUserPoolClient',
parameters: {
UserPoolId: this.userPool.userPoolId,
ClientId: this.userPoolClientId,
},
physicalResourceId: PhysicalResourceId.of(this.userPoolClientId),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: [this.userPool.userPoolArn],
}),
},
).getResponseField('UserPoolClient.ClientSecret'));
}

return this._userPoolClientSecret;
}

private configureAuthFlows(props: UserPoolClientProps): string[] | undefined {
if (!props.authFlows || Object.keys(props.authFlows).length === 0) return undefined;

Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-cognito/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
},
"dependencies": {
"@aws-cdk/aws-certificatemanager": "0.0.0",
"@aws-cdk/aws-secretsmanager": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-kms": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
Expand All @@ -102,6 +103,7 @@
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-certificatemanager": "0.0.0",
"@aws-cdk/aws-secretsmanager": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-kms": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Secret } from '@aws-cdk/aws-secretsmanager';
import { App, RemovalPolicy, Stack } from '@aws-cdk/core';
import { OAuthScope, UserPool, ClientAttributes, StringAttribute } from '../lib';
import { ClientAttributes, OAuthScope, StringAttribute, UserPool } from '../lib';

const app = new App();
const stack = new Stack(app, 'integ-user-pool-client-explicit-props');
Expand All @@ -12,7 +13,7 @@ const userpool = new UserPool(stack, 'myuserpool', {
},
});

userpool.addClient('myuserpoolclient', {
const client = userpool.addClient('myuserpoolclient', {
userPoolClientName: 'myuserpoolclient',
authFlows: {
adminUserPassword: true,
Expand Down Expand Up @@ -57,3 +58,7 @@ userpool.addClient('myuserpoolclient', {
website: true,
}).withCustomAttributes('attribute_one', 'attribute_two'),
});

new Secret(stack, 'Secret', {
secretStringValue: client.userPoolClientSecret,
});
Loading

0 comments on commit 67a24ba

Please sign in to comment.