Skip to content

Commit

Permalink
feat(cognito): user pool - identity provider attribute mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Niranjan Jayakar committed Jun 9, 2020
1 parent 480d4c0 commit 355d4f4
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 26 deletions.
25 changes: 21 additions & 4 deletions packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,26 @@ const provider = new UserPoolIdentityProviderAmazon(stack, 'Amazon', {
});
```

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.
Attribute mapping allows mapping attributes provided by the third-party identity providers to [standard and custom
attributes](#Attributes) of the user pool. Learn more about [Specifying Identity Provider Attribute Mappings for Your
User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html).

The following code shows how different attributes provided by 'Login With Amazon' can be mapped to standard and custom
user pool attributes.

```ts
new UserPoolIdentityProviderAmazon(stack, 'Amazon', {
// ...
attributeMapping: {
email: ProviderAttribute.AMAZON_EMAIL,
website: ProviderAttribute.custom('url'), // use custom() when an attribute is not pre-defined in the CDK
custom: {
// custom user pool attributes go here
uniqueId: ProviderAttribute.AMAZON_USER_ID,
}
}
});
```

### App Clients

Expand Down Expand Up @@ -456,7 +473,7 @@ pool.addClient('app-client', {

All identity providers created in the CDK app are automatically registered into the corresponding user pool. All app
clients created in the CDK have all of the identity providers enabled by default. The 'Cognito' identity provider,
that allows users to register and sign in directly with the Cognito user pool, is also enabled by default.
that allows users to register and sign in directly with the Cognito user pool, is also enabled by default.
Alternatively, the list of supported identity providers for a client can be explicitly specified -

```ts
Expand Down
19 changes: 19 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const StandardAttributeNames = {
address: 'address',
birthdate: 'birthdate',
email: 'email',
familyName: 'family_name',
gender: 'gender',
givenName: 'given_name',
locale: 'locale',
middleName: 'middle_name',
fullname: 'name',
nickname: 'nickname',
phoneNumber: 'phone_number',
profilePicture: 'picture',
preferredUsername: 'preferred_username',
profilePage: 'profile',
timezone: 'zoneinfo',
lastUpdateTime: 'updated_at',
website: 'website',
};
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase
client_secret: props.clientSecret,
authorize_scopes: scopes.join(' '),
},
attributeMapping: super.configureAttributeMapping(),
});

this.providerName = super.getResourceNameAttribute(resource.ref);
Expand Down
190 changes: 189 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,169 @@
import { Construct, Resource } from '@aws-cdk/core';
import { StandardAttributeNames } from '../private/attr-names';
import { IUserPool } from '../user-pool';
import { IUserPoolIdentityProvider } from '../user-pool-idp';

/**
* An attribute available from a third party identity provider.
*/
export class ProviderAttribute {
/** The user id attribute provided by Amazon */
public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id');
/** The email attribute provided by Amazon */
public static readonly AMAZON_EMAIL = new ProviderAttribute('email');
/** The name attribute provided by Amazon */
public static readonly AMAZON_NAME = new ProviderAttribute('name');
/** The postal code attribute provided by Amazon */
public static readonly AMAZON_POSTAL_CODE = new ProviderAttribute('postal_code');

/** The user id attribute provided by Facebook */
public static readonly FACEBOOK_ID = new ProviderAttribute('id');
/** The birthday attribute provided by Facebook */
public static readonly FACEBOOK_BIRTHDAY = new ProviderAttribute('birthday');
/** The email attribute provided by Facebook */
public static readonly FACEBOOK_EMAIL = new ProviderAttribute('email');
/** The name attribute provided by Facebook */
public static readonly FACEBOOK_NAME = new ProviderAttribute('name');
/** The first name attribute provided by Facebook */
public static readonly FACEBOOK_FIRST_NAME = new ProviderAttribute('first_name');
/** The last name attribute provided by Facebook */
public static readonly FACEBOOK_LAST_NAME = new ProviderAttribute('last_name');
/** The middle name attribute provided by Facebook */
public static readonly FACEBOOK_MIDDLE_NAME = new ProviderAttribute('middle_name');
/** The gender attribute provided by Facebook */
public static readonly FACEBOOK_GENDER = new ProviderAttribute('gender');
/** The locale attribute provided by Facebook */
public static readonly FACEBOOK_LOCALE = new ProviderAttribute('locale');

/**
* Use this to specify an attribute from the identity provider that is not pre-defined in the CDK.
* @param attributeName the attribute value string as recognized by the provider
*/
public static custom(attributeName: string): ProviderAttribute {
return new ProviderAttribute(attributeName);
}

/** The attribute value string as recognized by the provider. */
public readonly attributeName: string;

private constructor(attributeName: string) {
this.attributeName = attributeName;
}
}

/**
* The mapping of user pool attributes to the attributes provided by the identity providers.
*/
export interface AttributeMapping {
/**
* The user's postal address is a required attribute.
* @default - not mapped
*/
readonly address?: ProviderAttribute;

/**
* The user's birthday.
* @default - not mapped
*/
readonly birthdate?: ProviderAttribute;

/**
* The user's e-mail address.
* @default - not mapped
*/
readonly email?: ProviderAttribute;

/**
* The surname or last name of user.
* @default - not mapped
*/
readonly familyName?: ProviderAttribute;

/**
* The user's gender.
* @default - not mapped
*/
readonly gender?: ProviderAttribute;

/**
* The user's first name or give name.
* @default - not mapped
*/
readonly givenName?: ProviderAttribute;

/**
* The user's locale.
* @default - not mapped
*/
readonly locale?: ProviderAttribute;

/**
* The user's middle name.
* @default - not mapped
*/
readonly middleName?: ProviderAttribute;

/**
* The user's full name in displayable form.
* @default - not mapped
*/
readonly fullname?: ProviderAttribute;

/**
* The user's nickname or casual name.
* @default - not mapped
*/
readonly nickname?: ProviderAttribute;

/**
* The user's telephone number.
* @default - not mapped
*/
readonly phoneNumber?: ProviderAttribute;

/**
* The URL to the user's profile picture.
* @default - not mapped
*/
readonly profilePicture?: ProviderAttribute;

/**
* The user's preferred username.
* @default - not mapped
*/
readonly preferredUsername?: ProviderAttribute;

/**
* The URL to the user's profile page.
* @default - not mapped
*/
readonly profilePage?: ProviderAttribute;

/**
* The user's time zone.
* @default - not mapped
*/
readonly timezone?: ProviderAttribute;

/**
* Time, the user's information was last updated.
* @default - not mapped
*/
readonly lastUpdateTime?: ProviderAttribute;

/**
* The URL to the user's web page or blog.
* @default - not mapped
*/
readonly website?: ProviderAttribute;

/**
* Specify custom attribute mapping here and mapping for any standard attributes not supported yet.
* @default - no custom attribute mapping
*/
readonly custom?: { [key: string]: ProviderAttribute };
}

/**
* Properties to create a new instance of UserPoolIdentityProvider
*/
Expand All @@ -10,6 +172,12 @@ export interface UserPoolIdentityProviderProps {
* The user pool to which this construct provides identities.
*/
readonly userPool: IUserPool;

/**
* Mapping attributes from the identity provider to standard and custom attributes of the user pool.
* @default - no attribute mapping
*/
readonly attributeMapping?: AttributeMapping;
}

/**
Expand All @@ -18,8 +186,28 @@ export interface UserPoolIdentityProviderProps {
export abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider {
public abstract readonly providerName: string;

public constructor(scope: Construct, id: string, props: UserPoolIdentityProviderProps) {
public constructor(scope: Construct, id: string, private readonly props: UserPoolIdentityProviderProps) {
super(scope, id);
props.userPool.registerIdentityProvider(this);
}

protected configureAttributeMapping(): any {
if (!this.props.attributeMapping) {
return undefined;
}
type SansCustom = Omit<AttributeMapping, 'custom'>;
let mapping: { [key: string]: string } = {};
mapping = Object.entries(this.props.attributeMapping)
.filter(([k, _]) => k !== 'custom') // 'custom' handled later separately
.reduce((agg, [k, v]) => {
return { ...agg, [StandardAttributeNames[k as keyof SansCustom]]: v.attributeName };
}, mapping);
if (this.props.attributeMapping.custom) {
mapping = Object.entries(this.props.attributeMapping.custom).reduce((agg, [k, v]) => {
return { ...agg, [k]: v.attributeName };
}, mapping);
}
if (Object.keys(mapping).length === 0) { return undefined; }
return mapping;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBa
authorize_scopes: scopes.join(','),
api_version: props.apiVersion,
},
attributeMapping: super.configureAttributeMapping(),
});

this.providerName = super.getResourceNameAttribute(resource.ref);
Expand Down
21 changes: 1 addition & 20 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, Duration, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core';
import { CfnUserPool } from './cognito.generated';
import { StandardAttributeNames } from './private/attr-names';
import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr';
import { UserPoolClient, UserPoolClientOptions } from './user-pool-client';
import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain';
Expand Down Expand Up @@ -909,26 +910,6 @@ export class UserPool extends UserPoolBase {
}
}

const StandardAttributeNames: Record<keyof StandardAttributes, string> = {
address: 'address',
birthdate: 'birthdate',
email: 'email',
familyName: 'family_name',
gender: 'gender',
givenName: 'given_name',
locale: 'locale',
middleName: 'middle_name',
fullname: 'name',
nickname: 'nickname',
phoneNumber: 'phone_number',
profilePicture: 'picture',
preferredUsername: 'preferred_username',
profilePage: 'profile',
timezone: 'zoneinfo',
lastUpdateTime: 'updated_at',
website: 'website',
};

function undefinedIfNoKeys(struct: object): object | undefined {
const allUndefined = Object.values(struct).reduce((acc, v) => acc && (v === undefined), true);
return allUndefined ? undefined : struct;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@
"UserPoolId": {
"Ref": "pool056F3F7E"
},
"AttributeMapping": {
"given_name": "name",
"email": "email",
"userId": "user_id"
},
"ProviderDetails": {
"client_id": "amzn-client-id",
"client_secret": "amzn-client-secret",
Expand Down
9 changes: 8 additions & 1 deletion packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { App, CfnOutput, Stack } from '@aws-cdk/core';
import { UserPool, UserPoolIdentityProviderAmazon } from '../lib';
import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../lib';

/*
* Stack verification steps
Expand All @@ -15,6 +15,13 @@ new UserPoolIdentityProviderAmazon(stack, 'amazon', {
userPool: userpool,
clientId: 'amzn-client-id',
clientSecret: 'amzn-client-secret',
attributeMapping: {
givenName: ProviderAttribute.AMAZON_NAME,
email: ProviderAttribute.AMAZON_EMAIL,
custom: {
userId: ProviderAttribute.AMAZON_USER_ID,
},
},
});

const client = userpool.addClient('client');
Expand Down
Loading

0 comments on commit 355d4f4

Please sign in to comment.