Skip to content

Commit

Permalink
feat(cognito): user pool - OAuth2.0 authentication (#7141)
Browse files Browse the repository at this point in the history
Support for OAuth2.0 properties in the UserPoolClient construct.

BREAKING CHANGES: The type of the `authFlow` property in
`UserPoolClient` has changed. This is no longer an enum, but instead a
set of fields to toggle. The list has also been updated to reflect the
change from `ADMIN_NO_SRP_AUTH` to its newer alias
`ADMIN_USER_PASSWORD_AUTH`.
  • Loading branch information
Niranjan Jayakar authored Apr 6, 2020
1 parent 0ef6a95 commit 09852d0
Show file tree
Hide file tree
Showing 7 changed files with 681 additions and 27 deletions.
58 changes: 53 additions & 5 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' ],
}
});
```
216 changes: 201 additions & 15 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,185 @@ 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
*/
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;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}
26 changes: 21 additions & 5 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -515,6 +516,11 @@ export interface IUserPool extends IResource {
* @attribute
*/
readonly userPoolArn: string;

/**
* Create a user pool client.
*/
addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient;
}

/**
Expand All @@ -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);
}
Expand All @@ -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!);
}

/**
Expand Down Expand Up @@ -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`, {
Expand Down
Loading

0 comments on commit 09852d0

Please sign in to comment.