Skip to content

Commit

Permalink
feat(cognito): sign in url for a UserPoolDomain (#8155)
Browse files Browse the repository at this point in the history
Compute the sign in URL from a user pool domain, given a client.

The previous defaults on the UserPoolClient created one successfully but
was unusable since all of the features were turned off.
The defaults have been changed now so that the client created with the
defaults works out of the box.

BREAKING CHANGE: OAuth flows `authorizationCodeGrant` and
`implicitCodeGrant` in `UserPoolClient` are enabled by default.
* **cognito:** `callbackUrl` property in `UserPoolClient` is now
optional and has a default.
* **cognito:** All OAuth scopes in a `UserPoolClient` are now enabled
by default.


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Niranjan Jayakar authored May 29, 2020
1 parent 9ee61eb commit e942936
Show file tree
Hide file tree
Showing 13 changed files with 452 additions and 80 deletions.
30 changes: 29 additions & 1 deletion packages/@aws-cdk/aws-cognito/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,4 +446,32 @@ 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
Console](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html#cognito-user-pools-create-an-app-integration).

```ts
const userpool = new UserPool(this, 'UserPool', {
// ...
});
const client = userpool.addClient('Client', {
// ...
oAuth: {
flows: {
implicitCodeGrant: true,
},
callbackUrls: [
'https://myapp.com/home',
'https://myapp.com/users',
]
}
})
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
})
```
66 changes: 39 additions & 27 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@ 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
* @default {authorizationCodeGrant:true,implicitCodeGrant:true}
*/
readonly flows: OAuthFlows;
readonly flows?: OAuthFlows;

/**
* List of allowed redirect URLs for the identity providers.
* @default - no callback URLs
* @default - ['https://example.com'] if either authorizationCodeGrant or implicitCodeGrant flows are enabled, no callback URLs otherwise.
*/
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.
* @default [OAuthScope.PHONE,OAuthScope.EMAIL,OAuthScope.OPENID,OAuthScope.PROFILE,OAuthScope.COGNITO_ADMIN]
*/
readonly scopes: OAuthScope[];
readonly scopes?: OAuthScope[];
}

/**
Expand Down Expand Up @@ -221,6 +221,10 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
}

public readonly userPoolClientId: string;
/**
* The OAuth flows enabled for this client.
*/
public readonly oAuthFlows: OAuthFlows;
private readonly _userPoolClientName?: string;

/*
Expand All @@ -234,16 +238,31 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
constructor(scope: Construct, id: string, props: UserPoolClientProps) {
super(scope, id);

this.oAuthFlows = props.oAuth?.flows ?? {
implicitCodeGrant: true,
authorizationCodeGrant: true,
};

let callbackUrls: string[] | undefined = props.oAuth?.callbackUrls;
if (this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) {
if (callbackUrls === undefined) {
callbackUrls = [ 'https://example.com' ];
} else if (callbackUrls.length === 0) {
throw new Error('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.');
}
}

const resource = new CfnUserPoolClient(this, 'Resource', {
clientName: props.userPoolClientName,
generateSecret: props.generateSecret,
userPoolId: props.userPool.userPoolId,
explicitAuthFlows: this.configureAuthFlows(props),
allowedOAuthFlows: this.configureOAuthFlows(props.oAuth),
allowedOAuthFlows: this.configureOAuthFlows(),
allowedOAuthScopes: this.configureOAuthScopes(props.oAuth),
callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined,
callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined,
allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined,
preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors),
supportedIdentityProviders: [ 'COGNITO' ],
});

this.userPoolClientId = resource.ref;
Expand Down Expand Up @@ -275,37 +294,30 @@ export class UserPoolClient extends Resource implements IUserPoolClient {
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.');
}
private configureOAuthFlows(): string[] | undefined {
if ((this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) && this.oAuthFlows.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 (this.oAuthFlows.clientCredentials) { oAuthFlows.push('client_credentials'); }
if (this.oAuthFlows.implicitCodeGrant) { oAuthFlows.push('implicit'); }
if (this.oAuthFlows.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));
private configureOAuthScopes(oAuth?: OAuthSettings): string[] {
const scopes = oAuth?.scopes ?? [ OAuthScope.PROFILE, OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.OPENID,
OAuthScope.COGNITO_ADMIN ];
const scopeNames = new Set(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);
if (autoOpenIdScopes.reduce((agg, s) => agg || scopeNames.has(s.scopeName), false)) {
scopeNames.add(OAuthScope.OPENID.scopeName);
}
return undefined;
return Array.from(scopeNames);
}

private configurePreventUserExistenceErrors(prevent?: boolean): string | undefined {
Expand Down
50 changes: 49 additions & 1 deletion packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ICertificate } from '@aws-cdk/aws-certificatemanager';
import { Construct, IResource, Resource } from '@aws-cdk/core';
import { Construct, IResource, Resource, Stack } from '@aws-cdk/core';
import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources';
import { CfnUserPoolDomain } from './cognito.generated';
import { IUserPool } from './user-pool';
import { UserPoolClient } from './user-pool-client';

/**
* Represents a user pool domain.
Expand Down Expand Up @@ -80,6 +81,7 @@ export interface UserPoolDomainProps extends UserPoolDomainOptions {
*/
export class UserPoolDomain extends Resource implements IUserPoolDomain {
public readonly domainName: string;
private isCognitoDomain: boolean;

constructor(scope: Construct, id: string, props: UserPoolDomainProps) {
super(scope, id);
Expand All @@ -92,6 +94,8 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain {
throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens');
}

this.isCognitoDomain = !!props.cognitoDomain;

const domainName = props.cognitoDomain?.domainPrefix || props.customDomain?.domainName!;
const resource = new CfnUserPoolDomain(this, 'Resource', {
userPoolId: props.userPool.userPoolId,
Expand Down Expand Up @@ -126,4 +130,48 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain {
});
return customResource.getResponseField('DomainDescription.CloudFrontDistribution');
}

/**
* The URL to the hosted UI associated with this domain
*/
public baseUrl(): string {
if (this.isCognitoDomain) {
return `https://${this.domainName}.auth.${Stack.of(this).region}.amazoncognito.com`;
}
return `https://${this.domainName}`;
}

/**
* The URL to the sign in page in this domain using a specific UserPoolClient
* @param client [disable-awslint:ref-via-interface] the user pool client that the UI will use to interact with the UserPool
* @param options options to customize the behaviour of this method.
*/
public signInUrl(client: UserPoolClient, options: SignInUrlOptions): string {
let responseType: string;
if (client.oAuthFlows.authorizationCodeGrant) {
responseType = 'code';
} else if (client.oAuthFlows.implicitCodeGrant) {
responseType = 'token';
} else {
throw new Error('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled');
}
const path = options.signInPath ?? '/login';
return `${this.baseUrl()}${path}?client_id=${client.userPoolClientId}&response_type=${responseType}&redirect_uri=${options.redirectUri}`;
}
}

/**
* Options to customize the behaviour of `signInUrl()`
*/
export interface SignInUrlOptions {
/**
* Where to redirect to after sign in
*/
readonly redirectUri: string;

/**
* The path in the URI where the sign-in page is located
* @default '/login'
*/
readonly signInPath?: string;
}
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-cognito/lib/user-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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';
import { UserPoolClient, UserPoolClientOptions } from './user-pool-client';
import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain';

/**
Expand Down Expand Up @@ -529,7 +529,7 @@ export interface IUserPool extends IResource {
* Add a new app client to this user pool.
* @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html
*/
addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient;
addClient(id: string, options?: UserPoolClientOptions): UserPoolClient;

/**
* Associate a domain to this user pool.
Expand All @@ -542,7 +542,7 @@ abstract class UserPoolBase extends Resource implements IUserPool {
public abstract readonly userPoolId: string;
public abstract readonly userPoolArn: string;

public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient {
public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient {
return new UserPoolClient(this, id, {
userPool: this,
...options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@
"email",
"openid",
"profile",
"aws.cognito.signin.user.admin",
"my-resource-server/my-scope"
"aws.cognito.signin.user.admin"
],
"CallbackURLs": [
"https://redirect-here.myapp.com"
Expand All @@ -94,8 +93,11 @@
"ALLOW_REFRESH_TOKEN_AUTH"
],
"GenerateSecret": true,
"PreventUserExistenceErrors": "ENABLED"
"PreventUserExistenceErrors": "ENABLED",
"SupportedIdentityProviders": [
"COGNITO"
]
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ userpool.addClient('myuserpoolclient', {
OAuthScope.OPENID,
OAuthScope.PROFILE,
OAuthScope.COGNITO_ADMIN,
OAuthScope.custom('my-resource-server/my-scope'),
],
callbackUrls: [ 'https://redirect-here.myapp.com' ],
},
Expand Down
Loading

0 comments on commit e942936

Please sign in to comment.