Skip to content

Commit

Permalink
feat(cognito): allow to set read and write attributes in Cognito User…
Browse files Browse the repository at this point in the history
…PoolClient (#7607)

### Commit Message

feat(cognito): allow to set read and write attributes in Cognito UserPoolClient

Add ability to specify which attributes each App Client can read or write.

closes #7407

### End Commit Message
----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
tom139 committed Jan 20, 2021
1 parent d613447 commit 552e1e9
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 4 deletions.
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Expand Up @@ -572,6 +572,30 @@ pool.addClient('app-client', {
});
```

Clients can (and should) be allowed to read and write relevant user attributes only. Usually every client can be allowed to read the `given_name`
attribute but not every client should be allowed to set the `email_verified` attribute.
The same criteria applies for both standard and custom attributes, more info is available at
[Attribute Permissions and Scopes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes).
The default behaviour is to allow read and write permissions on all attributes. The following code shows how this can be configured for a client.

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

const clientWriteAttributes = (new ClientAttributes())
.withStandardAttributes({name: true, email: true})
.withCustomAttributes(['favouritePizza']);

const clientReadAttributes = clientWriteAttributes
.withStandardAttributes({emailVerified: true})
.withCustomAttributes(['pointsEarned']);

pool.addClient('app-client', {
// ...
readAttributes: clientReadAttributes,
writeAttributes: clientWriteAttributes,
});
```

### Resource Servers

A resource server is a server for access-protected resources. It handles authenticated requests from an app that has an
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts
Expand Up @@ -16,4 +16,6 @@ export const StandardAttributeNames = {
timezone: 'zoneinfo',
lastUpdateTime: 'updated_at',
website: 'website',
emailVerified: 'email_verified',
phoneNumberVerified: 'phone_number_verified',
};
200 changes: 200 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts
@@ -1,4 +1,5 @@
import { Token } from '@aws-cdk/core';
import { StandardAttributeNames } from './private/attr-names';

/**
* The set of standard attributes that can be marked as required or mutable.
Expand Down Expand Up @@ -107,6 +108,18 @@ export interface StandardAttributes {
* @default - see the defaults under `StandardAttribute`
*/
readonly website?: StandardAttribute;

/**
* Whether the email address has been verified.
* @default - see the defaults under `StandardAttribute`
*/
readonly emailVerified?: StandardAttribute;

/**
* Whether the phone number has been verified.
* @default - see the defaults under `StandardAttribute`
*/
readonly phoneNumberVerified?: StandardAttribute;
}

/**
Expand Down Expand Up @@ -341,3 +354,190 @@ export class DateTimeAttribute implements ICustomAttribute {
};
}
}

/**
* This interface contains all standard attributes recognized by Cognito
* from https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html
* including `email_verified` and `phone_number_verified`
*/
export interface StandardAttributesMask {
/**
* The user's postal address.
* @default false
*/
readonly address?: boolean;

/**
* The user's birthday, represented as an ISO 8601:2004 format.
* @default false
*/
readonly birthdate?: boolean;

/**
* The user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec.
* @default false
*/
readonly email?: boolean;

/**
* The surname or last name of the user.
* @default false
*/
readonly familyName?: boolean;

/**
* The user's gender.
* @default false
*/
readonly gender?: boolean;

/**
* The user's first name or give name.
* @default false
*/
readonly givenName?: boolean;

/**
* The user's locale, represented as a BCP47 [RFC5646] language tag.
* @default false
*/
readonly locale?: boolean;

/**
* The user's middle name.
* @default false
*/
readonly middleName?: boolean;

/**
* The user's full name in displayable form, including all name parts, titles and suffixes.
* @default false
*/
readonly fullname?: boolean;

/**
* The user's nickname or casual name.
* @default false
*/
readonly nickname?: boolean;

/**
* The user's telephone number.
* @default false
*/
readonly phoneNumber?: boolean;

/**
* The URL to the user's profile picture.
* @default false
*/
readonly profilePicture?: boolean;

/**
* The user's preffered username, different from the immutable user name.
* @default false
*/
readonly preferredUsername?: boolean;

/**
* The URL to the user's profile page.
* @default false
*/
readonly profilePage?: boolean;

/**
* The user's time zone.
* @default false
*/
readonly timezone?: boolean;

/**
* The time, the user's information was last updated.
* @default false
*/
readonly lastUpdateTime?: boolean;

/**
* The URL to the user's web page or blog.
* @default false
*/
readonly website?: boolean;

/**
* Whether the email address has been verified.
* @default false
*/
readonly emailVerified?: boolean;

/**
* Whether the phone number has been verified.
* @default false
*/
readonly phoneNumberVerified?: boolean;
}


/**
* A set of attributes, useful to set Read and Write attributes
*/
export class ClientAttributes {

/**
* The set of attributes
*/
private attributesSet: Set<string>;

/**
* Creates a ClientAttributes with the specified attributes
*
* @default - a ClientAttributes object without any attributes
*/
constructor() {
this.attributesSet = new Set<string>();
}

/**
* Creates a custom ClientAttributes with the specified attributes
* @param attributes a list of standard attributes to add to the set
*/
public withStandardAttributes(attributes: StandardAttributesMask): ClientAttributes {
let attributesSet = new Set(this.attributesSet);
// iterate through key-values in the `StandardAttributeNames` constant
// to get the value for all attributes
for (const attributeKey in StandardAttributeNames) {
if ((attributes as any)[attributeKey] === true) {
const attributeName = (StandardAttributeNames as any)[attributeKey];
attributesSet.add(attributeName);
}
}
let aux = new ClientAttributes();
aux.attributesSet = attributesSet;
return aux;
}

/**
* Creates a custom ClientAttributes with the specified attributes
* @param attributes a list of custom attributes to add to the set
*/
public withCustomAttributes(...attributes: string[]): ClientAttributes {
let attributesSet: Set<string> = new Set(this.attributesSet);
for (let attribute of attributes) {
// custom attributes MUST begin with `custom:`, so add the string if not present
if (!attribute.startsWith('custom:')) {
attribute = 'custom:' + attribute;
}
attributesSet.add(attribute);
}
let aux = new ClientAttributes();
aux.attributesSet = attributesSet;
return aux;
}

/**
* The list of attributes represented by this ClientAttributes
*/
public attributes(): string[] {
// sorting is unnecessary but it simplify testing
return Array.from(this.attributesSet).sort();
}
}
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Expand Up @@ -2,6 +2,7 @@ import { IResource, Resource, Duration } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnUserPoolClient } from './cognito.generated';
import { IUserPool } from './user-pool';
import { ClientAttributes } from './user-pool-attr';
import { IUserPoolResourceServer, ResourceServerScope } from './user-pool-resource-server';

/**
Expand Down Expand Up @@ -272,6 +273,20 @@ export interface UserPoolClientOptions {
* @default Duration.minutes(60)
*/
readonly accessTokenValidity?: Duration;

/**
* The set of attributes this client will be able to read.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes
* @default - all standard and custom attributes
*/
readonly readAttributes?: ClientAttributes;

/**
* The set of attributes this client will be able to write.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-attribute-permissions-and-scopes
* @default - all standard and custom attributes
*/
readonly writeAttributes?: ClientAttributes;
}

/**
Expand Down Expand Up @@ -358,6 +373,8 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
allowedOAuthFlowsUserPoolClient: !props.disableOAuth,
preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors),
supportedIdentityProviders: this.configureIdentityProviders(props),
readAttributes: props.readAttributes?.attributes(),
writeAttributes: props.writeAttributes?.attributes(),
});
this.configureTokenValidity(resource, props);

Expand Down
Expand Up @@ -62,6 +62,11 @@
"PreventUserExistenceErrors": "ENABLED",
"SupportedIdentityProviders": [
"COGNITO"
],
"WriteAttributes": [
"address", "birthdate", "custom:attribute_one", "custom:attribute_two", "email",
"family_name", "gender", "given_name", "locale", "middle_name", "name", "nickname", "phone_number",
"picture", "preferred_username", "profile", "updated_at", "website", "zoneinfo"
]
}
}
Expand Down
@@ -1,5 +1,5 @@
import { App, Stack } from '@aws-cdk/core';
import { OAuthScope, UserPool } from '../lib';
import { OAuthScope, UserPool, ClientAttributes } from '../lib';

const app = new App();
const stack = new Stack(app, 'integ-user-pool-client-explicit-props');
Expand Down Expand Up @@ -30,4 +30,24 @@ userpool.addClient('myuserpoolclient', {
callbackUrls: ['https://redirect-here.myapp.com'],
},
preventUserExistenceErrors: true,
writeAttributes: (new ClientAttributes()).withStandardAttributes(
{
address: true,
birthdate: true,
email: true,
familyName: true,
fullname: true,
gender: true,
givenName: true,
lastUpdateTime: true,
locale: true,
middleName: true,
nickname: true,
phoneNumber: true,
preferredUsername: true,
profilePage: true,
profilePicture: true,
timezone: true,
website: true,
}).withCustomAttributes('attribute_one', 'attribute_two'),
});

0 comments on commit 552e1e9

Please sign in to comment.