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): user pool identity provider with support for Facebook & Amazon #8134

Merged
merged 14 commits into from
Jun 3, 2020
35 changes: 33 additions & 2 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw
- [Lambda Triggers](#lambda-triggers)
- [Import](#importing-user-pools)
- [App Clients](#app-clients)
- [Identity Providers](#identity-providers)
- [Domains](#domains)

## User Pools
Expand Down Expand Up @@ -417,6 +418,36 @@ pool.addClient('app-client', {
});
```

### Identity Providers

Users that are part of a user pool can sign in either directly through a user pool, or federate through a third-party
identity provider. Once configured, the Cognito backend will take care of integrating with the third-party provider.
Read more about [Adding User Pool Sign-in Through a Third
Party](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html).

The following third-party identity providers are currentlhy supported in the CDK -

* [Login With Amazon](https://developer.amazon.com/apps-and-games/login-with-amazon)
* [Facebook Login](https://developers.facebook.com/docs/facebook-login/)

The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity
provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the
third-party identity provider.

```ts
const userpool = new UserPool(stack, 'Pool');

const provider = UserPoolIdentityProvider.amazon(stack, 'Amazon', {
clientId: 'amzn-client-id',
clientSecret: 'amzn-client-secret',
userPool: userpool,
});
```

In order to allow users to sign in with a third-party identity provider, the app client that faces the user should be
configured to use the identity provider. See [App Clients](#app-clients) section to know more about App Clients.
The identity providers should be configured on `identityProviders` property available on the `UserPoolClient` construct.

### Domains

After setting up an [app client](#app-clients), the address for the user pool's sign-up and sign-in webpages can be
Expand Down Expand Up @@ -446,7 +477,7 @@ pool.addDomain('CustomDomain', {

Read more about [Using the Amazon Cognito
Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own
Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html)
Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html).

The `signInUrl()` methods returns the fully qualified URL to the login page for the user pool. This page comes from the
hosted UI configured with Cognito. Learn more at [Hosted UI with the Amazon Cognito
Expand Down Expand Up @@ -474,4 +505,4 @@ const domain = userpool.addDomain('Domain', {
const signInUrl = domain.signInUrl(client, {
redirectUrl: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client
})
```
```
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export * from './cognito.generated';
export * from './user-pool';
export * from './user-pool-attr';
export * from './user-pool-client';
export * from './user-pool-domain';
export * from './user-pool-domain';
export * from './user-pool-idp';
export * from './user-pool-idps';
49 changes: 48 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,27 @@ export class OAuthScope {
}
}

/**
* Identity providers supported by the UserPoolClient
*/
export interface SupportedIdentityProviders {
/**
* Whether users can sign in directly as a user of the User Pool.
* @default true
*/
readonly cognito?: boolean;
/**
* Whether users can sign in using 'Facebook Login'.
* @default false
*/
readonly facebook?: boolean;
/**
* Whether users can sign in using 'Login With Amazon'.
* @default false
*/
readonly amazon?: boolean;
}

/**
* Options to create a UserPoolClient
*/
Expand Down Expand Up @@ -182,6 +203,15 @@ export interface UserPoolClientOptions {
* @default true for new stacks
*/
readonly preventUserExistenceErrors?: boolean;

/**
* The list of identity providers that users should be able to use to sign in using this client.
*
* @default - supports all identity providers that are registered with the user pool. If the user pool and/or
* identity providers are imported, either specify this option explicitly or ensure that the identity providers are
* registered with the user pool using the `UserPool.registerIdentityProvider()` API.
*/
readonly supportedIdentityProviders?: SupportedIdentityProviders;
}

/**
Expand Down Expand Up @@ -262,7 +292,7 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined,
allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined,
preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors),
supportedIdentityProviders: [ 'COGNITO' ],
supportedIdentityProviders: this.configureIdentityProviders(props),
});

this.userPoolClientId = resource.ref;
Expand Down Expand Up @@ -326,4 +356,21 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
}
return prevent ? 'ENABLED' : 'LEGACY';
}

private configureIdentityProviders(props: UserPoolClientProps): string[] | undefined {
let providers: string[];
if (!props.supportedIdentityProviders) {
const providerSet = new Set(props.userPool.identityProviders.map((p) => p.providerName));
providerSet.add('COGNITO');
providers = Array.from(providerSet);
} else {
providers = [];
const idps = props.supportedIdentityProviders;
if (idps.cognito === undefined || idps.cognito === true) { providers.push('COGNITO'); }
if (idps.facebook) { providers.push('Facebook'); }
if (idps.amazon) { providers.push('LoginWithAmazon'); }
}
if (providers.length === 0) { return undefined; }
return Array.from(providers);
}
}
51 changes: 51 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Construct, IResource, Resource } from '@aws-cdk/core';
import {
UserPoolIdentityProviderAmazon,
UserPoolIdentityProviderAmazonProps,
UserPoolIdentityProviderFacebook,
UserPoolIdentityProviderFacebookProps,
} from './user-pool-idps';

/**
* Represents a UserPoolIdentityProvider
*/
export interface IUserPoolIdentityProvider extends IResource {
/**
* The primary identifier of this identity provider
* @attribute
*/
readonly providerName: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be typed UserPoolClientIdentityProvider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not so confident that they are one and the same. They are the same at the moment, for the providers I've chosen to implement.

Ex: the list here does not match up with here.

It's not clear yet if this is a documentation gap, or something more.

Leaving this as is for now.

}

/**
* User pool third-party identity providers
*/
export class UserPoolIdentityProvider {

/**
* Import an existing UserPoolIdentityProvider
*/
public static fromProviderName(scope: Construct, id: string, providerName: string): IUserPoolIdentityProvider {
class Import extends Resource implements IUserPoolIdentityProvider {
public readonly providerName: string = providerName;
}

return new Import(scope, id);
}

/**
* Federate login with 'Login with Amazon'
*/
public static amazon(scope: Construct, id: string, props: UserPoolIdentityProviderAmazonProps) {
return new UserPoolIdentityProviderAmazon(scope, id, props);
}

/**
* Federate login with 'Facebook Login'
*/
public static facebook(scope: Construct, id: string, props: UserPoolIdentityProviderFacebookProps) {
return new UserPoolIdentityProviderFacebook(scope, id, props);
}

private constructor() {}
}
52 changes: 52 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Construct } from '@aws-cdk/core';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';
import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base';

/**
* Properties to initialize UserPoolAmazonIdentityProvider
*/
export interface UserPoolIdentityProviderAmazonProps extends UserPoolIdentityProviderProps {
/**
* The client id recognized by 'Login with Amazon' APIs.
* @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier
*/
readonly clientId: string;
/**
* The client secret to be accompanied with clientId for 'Login with Amazon' APIs to authenticate the client.
* @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier
*/
readonly clientSecret: string;
/**
* The types of user profile data to obtain for the Amazon profile.
* @see https://developer.amazon.com/docs/login-with-amazon/customer-profile.html
* @default [ profile ]
*/
readonly scopes?: string[];
}

/**
* Represents a identity provider that integrates with 'Login with Amazon'
* @resource AWS::Cognito::UserPoolIdentityProvider
*/
export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase {
public readonly providerName: string;

constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAmazonProps) {
super(scope, id, props);

const scopes = props.scopes ?? [ 'profile' ];

const resource = new CfnUserPoolIdentityProvider(this, 'Resource', {
userPoolId: props.userPool.userPoolId,
providerName: 'LoginWithAmazon', // must be 'LoginWithAmazon' when the type is 'LoginWithAmazon'
providerType: 'LoginWithAmazon',
providerDetails: {
client_id: props.clientId,
client_secret: props.clientSecret,
authorize_scopes: scopes.join(' '),
},
});

this.providerName = super.getResourceNameAttribute(resource.ref);
}
}
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Construct, Resource } from '@aws-cdk/core';
import { IUserPool } from '../user-pool';
import { IUserPoolIdentityProvider } from '../user-pool-idp';

/**
* Properties to create a new instance of UserPoolIdentityProvider
*/
export interface UserPoolIdentityProviderProps {
/**
* The user pool to which this construct provides identities.
*/
readonly userPool: IUserPool;
}

/**
* Options to integrate with the various social identity providers.
*/
export abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider {
public abstract readonly providerName: string;

public constructor(scope: Construct, id: string, props: UserPoolIdentityProviderProps) {
super(scope, id);
props.userPool.registerIdentityProvider(this);
}
}
57 changes: 57 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Construct } from '@aws-cdk/core';
import { CfnUserPoolIdentityProvider } from '../cognito.generated';
import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base';

/**
* Properties to initialize UserPoolFacebookIdentityProvider
*/
export interface UserPoolIdentityProviderFacebookProps extends UserPoolIdentityProviderProps {
/**
* The client id recognized by Facebook APIs.
*/
readonly clientId: string;
/**
* The client secret to be accompanied with clientUd for Facebook to authenticate the client.
* @see https://developers.facebook.com/docs/facebook-login/security#appsecret
*/
readonly clientSecret: string;
/**
* The list of facebook permissions to obtain for getting access to the Facebook profile.
* @see https://developers.facebook.com/docs/facebook-login/permissions
* @default [ public_profile ]
*/
readonly scopes?: string[];
/**
* The Facebook API version to use
* @default - to the oldest version supported by Facebook
*/
readonly apiVersion?: string;
}

/**
* Represents a identity provider that integrates with 'Facebook Login'
* @resource AWS::Cognito::UserPoolIdentityProvider
*/
export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBase {
public readonly providerName: string;

constructor(scope: Construct, id: string, props: UserPoolIdentityProviderFacebookProps) {
super(scope, id, props);

const scopes = props.scopes ?? [ 'public_profile' ];

const resource = new CfnUserPoolIdentityProvider(this, 'Resource', {
userPoolId: props.userPool.userPoolId,
providerName: 'Facebook', // must be 'Facebook' when the type is 'Facebook'
providerType: 'Facebook',
providerDetails: {
client_id: props.clientId,
client_secret: props.clientSecret,
authorize_scopes: scopes.join(','),
api_version: props.apiVersion,
},
});

this.providerName = super.getResourceNameAttribute(resource.ref);
}
}
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './base';
export * from './amazon';
export * from './facebook';
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CfnUserPool } from './cognito.generated';
import { ICustomAttribute, RequiredAttributes } from './user-pool-attr';
import { UserPoolClient, UserPoolClientOptions } from './user-pool-client';
import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain';
import { IUserPoolIdentityProvider } from './user-pool-idp';

/**
* The different ways in which users of this pool can sign up or sign in.
Expand Down Expand Up @@ -525,6 +526,11 @@ export interface IUserPool extends IResource {
*/
readonly userPoolArn: string;

/**
* Get all identity providers registered with this user pool.
*/
readonly identityProviders: IUserPoolIdentityProvider[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Wonder if this couldn't/shouldn't have just been a list of UserPoolClientIdentityProviders.

I guess not because of the Client in there... would have been simpler to have been a list of strings... but no worries I guess.

Copy link
Contributor

Choose a reason for hiding this comment

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

BTW I think I feel more for setting this list upon UserPool.fromAttributes() than doing UserPool.fromName() and calling registerIdentityProvider() multiple times.

Since it's not really mutating but rather giving the construct lib more information about the thing that's already there, feels like it should be a part of the import.


/**
* Add a new app client to this user pool.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html
Expand All @@ -536,11 +542,17 @@ export interface IUserPool extends IResource {
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html
*/
addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain;

/**
* Register an identity provider with this user pool.
*/
registerIdentityProvider(provider: IUserPoolIdentityProvider): void;
}

abstract class UserPoolBase extends Resource implements IUserPool {
public abstract readonly userPoolId: string;
public abstract readonly userPoolArn: string;
public readonly identityProviders: IUserPoolIdentityProvider[] = [];

public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient {
return new UserPoolClient(this, id, {
Expand All @@ -555,6 +567,10 @@ abstract class UserPoolBase extends Resource implements IUserPool {
...options,
});
}

public registerIdentityProvider(provider: IUserPoolIdentityProvider) {
this.identityProviders.push(provider);
}
}

/**
Expand Down
Loading