From 49ede22b35e54b0aa4541964df84aa4b4e76a985 Mon Sep 17 00:00:00 2001 From: asjadsaboor <11973227+asjadsaboor@users.noreply.github.com> Date: Thu, 15 Oct 2020 16:51:11 +0500 Subject: [PATCH] feat(cognito): user pool identity support for Google (#10649) Added support for Google in cognito user pool identity. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cognito/README.md | 1 + .../aws-cognito/lib/user-pool-client.ts | 6 + .../aws-cognito/lib/user-pool-idps/base.ts | 19 +++ .../aws-cognito/lib/user-pool-idps/google.ts | 53 ++++++++ .../aws-cognito/lib/user-pool-idps/index.ts | 3 +- packages/@aws-cdk/aws-cognito/package.json | 3 +- ...teg.user-pool-explicit-props.expected.json | 32 +++-- .../test/integ.user-pool-explicit-props.ts | 6 +- ... integ.user-pool-idp.amazon.expected.json} | 0 ...l-idp.ts => integ.user-pool-idp.amazon.ts} | 2 +- .../integ.user-pool-idp.google.expected.json | 117 ++++++++++++++++++ .../test/integ.user-pool-idp.google.ts | 41 ++++++ .../aws-cognito/test/user-pool-client.test.ts | 3 +- .../test/user-pool-idps/google.test.ts | 101 +++++++++++++++ 14 files changed, 361 insertions(+), 26 deletions(-) create mode 100644 packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts rename packages/@aws-cdk/aws-cognito/test/{integ.user-pool-idp.expected.json => integ.user-pool-idp.amazon.expected.json} (100%) rename packages/@aws-cdk/aws-cognito/test/{integ.user-pool-idp.ts => integ.user-pool-idp.amazon.ts} (94%) create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.expected.json create mode 100644 packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.ts create mode 100644 packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 658dfcf3e6a85..749ca525f048c 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -374,6 +374,7 @@ 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/) +* [Google Login](https://developers.google.com/identity/sign-in/web/sign-in) 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 diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index 0e59e5c2a6d2b..630000df485ba 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -154,6 +154,12 @@ export class UserPoolClientIdentityProvider { */ public static readonly FACEBOOK = new UserPoolClientIdentityProvider('Facebook'); + /** + * Allow users to sign in using 'Google Login'. + * A `UserPoolIdentityProviderGoogle` must be attached to the user pool. + */ + public static readonly GOOGLE = new UserPoolClientIdentityProvider('Google'); + /** * Allow users to sign in using 'Login With Amazon'. * A `UserPoolIdentityProviderAmazon` must be attached to the user pool. diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts index 6987050e65e82..e32f59eca2de5 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -36,6 +36,25 @@ export class ProviderAttribute { /** The locale attribute provided by Facebook */ public static readonly FACEBOOK_LOCALE = new ProviderAttribute('locale'); + /** The name attribute provided by Google */ + public static readonly GOOGLE_NAMES = new ProviderAttribute('names'); + /** The gender attribute provided by Google */ + public static readonly GOOGLE_GENDER = new ProviderAttribute('gender'); + /** The birthday attribute provided by Google */ + public static readonly GOOGLE_BIRTHDAYS = new ProviderAttribute('birthdays'); + /** The birthday attribute provided by Google */ + public static readonly GOOGLE_PHONE_NUMBERS = new ProviderAttribute('phoneNumbers'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Google */ + public static readonly GOOGLE_NAME = new ProviderAttribute('name'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_PICTURE = new ProviderAttribute('picture'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_GIVEN_NAME = new ProviderAttribute('given_name'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_FAMILY_NAME = new ProviderAttribute('family_name'); + /** * 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 diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts new file mode 100644 index 0000000000000..12275646691e8 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts @@ -0,0 +1,53 @@ +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base'; + +/** + * Properties to initialize UserPoolGoogleIdentityProvider + */ +export interface UserPoolIdentityProviderGoogleProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Google APIs. + * @see https://developers.google.com/identity/sign-in/web/sign-in#specify_your_apps_client_id + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientId for Google APIs to authenticate the client. + * @see https://developers.google.com/identity/sign-in/web/sign-in + */ + readonly clientSecret: string; + /** + * The list of google permissions to obtain for getting access to the google profile + * @see https://developers.google.com/identity/sign-in/web/sign-in + * @default [ profile ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Google' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderGoogle extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderGoogleProps) { + super(scope, id, props); + + const scopes = props.scopes ?? ['profile']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'Google', // must be 'Google' when the type is 'Google' + providerType: 'Google', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(' '), + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts index e0efb718962c4..dbc63a9854f37 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -1,3 +1,4 @@ export * from './base'; export * from './amazon'; -export * from './facebook'; \ No newline at end of file +export * from './facebook'; +export * from './google'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 8a9dd34fb536f..da7da1d44691c 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -106,7 +106,8 @@ "resource-attribute:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientClientSecret", "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps", - "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps" + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json index 5e2c8662f3f60..ed77c3b7baa53 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json @@ -680,10 +680,16 @@ "myuserpool01998219": { "Type": "AWS::Cognito::UserPool", "Properties": { - "AccountRecoverySetting": { + "AccountRecoverySetting": { "RecoveryMechanisms": [ - { "Name": "verified_phone_number", "Priority": 1 }, - { "Name": "verified_email", "Priority": 2 } + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } ] }, "AdminCreateUserConfig": { @@ -701,16 +707,8 @@ "email", "phone_number" ], - "EmailConfiguration": { - "From": "noreply@myawesomeapp.com", - "ReplyToEmailAddress": "support@myawesomeapp.com" - }, "EmailVerificationMessage": "verification email body from the integ test. Code is {####}.", "EmailVerificationSubject": "verification email subject from the integ test", - "EnabledMfas": [ - "SMS_MFA", - "SOFTWARE_TOKEN_MFA" - ], "LambdaConfig": { "CreateAuthChallenge": { "Fn::GetAtt": [ @@ -773,7 +771,7 @@ ] } }, - "MfaConfiguration": "ON", + "MfaConfiguration": "OFF", "Policies": { "PasswordPolicy": { "MinimumLength": 12, @@ -786,14 +784,14 @@ }, "Schema": [ { + "Mutable": true, "Name": "name", - "Required": true, - "Mutable": true + "Required": true }, { + "Mutable": true, "Name": "email", - "Required": true, - "Mutable": true + "Required": true }, { "AttributeDataType": "String", @@ -881,4 +879,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts index 1f4f7fe8193c5..261251e1e2592 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts @@ -43,7 +43,7 @@ const userpool = new UserPool(stack, 'myuserpool', { 'some-boolean-attr': new BooleanAttribute(), 'some-datetime-attr': new DateTimeAttribute(), }, - mfa: Mfa.REQUIRED, + mfa: Mfa.OFF, mfaSecondFactor: { sms: true, otp: true, @@ -56,10 +56,6 @@ const userpool = new UserPool(stack, 'myuserpool', { requireUppercase: true, requireSymbols: true, }, - emailSettings: { - from: 'noreply@myawesomeapp.com', - replyTo: 'support@myawesomeapp.com', - }, lambdaTriggers: { createAuthChallenge: dummyTrigger('createAuthChallenge'), customMessage: dummyTrigger('customMessage'), diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.amazon.expected.json similarity index 100% rename from packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json rename to packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.amazon.expected.json diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.amazon.ts similarity index 94% rename from packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts rename to packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.amazon.ts index 31804f1ce95e8..7252dfc0a8a40 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.amazon.ts @@ -7,7 +7,7 @@ import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../ * * If you plug in valid 'Login with Amazon' credentials, the federated log in should work. */ const app = new App(); -const stack = new Stack(app, 'integ-user-pool-idp'); +const stack = new Stack(app, 'integ-user-pool-idp-amazon'); const userpool = new UserPool(stack, 'pool'); diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.expected.json new file mode 100644 index 0000000000000..c767ee9d2d260 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.expected.json @@ -0,0 +1,117 @@ +{ + "Resources": { + "pool056F3F7E": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "poolclient2623294C": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "googleDB2C5242" + }, + "COGNITO" + ] + } + }, + "pooldomain430FA744": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "nija-test-pool", + "UserPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "googleDB2C5242": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "Google", + "ProviderType": "Google", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AttributeMapping": { + "given_name": "given_name", + "family_name": "family_name", + "email": "email", + "gender": "gender", + "names": "names" + }, + "ProviderDetails": { + "client_id": "google-client-id", + "client_secret": "google-client-secret", + "authorize_scopes": "profile" + } + } + } + }, + "Outputs": { + "SignInLink": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "pooldomain430FA744" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "poolclient2623294C" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.ts new file mode 100644 index 0000000000000..fac20b8351d38 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.google.ts @@ -0,0 +1,41 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderGoogle } from '../lib'; + +/* + * Stack verification steps + * * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'Google' sign in link shows up. + * * If you plug in valid 'Google' credentials, the federated log in should work. + */ +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-idp-google'); + +const userpool = new UserPool(stack, 'pool'); + +new UserPoolIdentityProviderGoogle(stack, 'google', { + userPool: userpool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + attributeMapping: { + givenName: ProviderAttribute.GOOGLE_GIVEN_NAME, + familyName: ProviderAttribute.GOOGLE_FAMILY_NAME, + email: ProviderAttribute.GOOGLE_EMAIL, + gender: ProviderAttribute.GOOGLE_GENDER, + custom: { + names: ProviderAttribute.GOOGLE_NAMES, + }, + }, +}); + +const client = userpool.addClient('client'); + +const domain = userpool.addDomain('domain', { + cognitoDomain: { + domainPrefix: 'nija-test-pool', + }, +}); + +new CfnOutput(stack, 'SignInLink', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 081d9d9e6526b..eeddb55bc1ff1 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -451,13 +451,14 @@ describe('User Pool Client', () => { UserPoolClientIdentityProvider.COGNITO, UserPoolClientIdentityProvider.FACEBOOK, UserPoolClientIdentityProvider.AMAZON, + UserPoolClientIdentityProvider.GOOGLE, ], }); // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { ClientName: 'AllEnabled', - SupportedIdentityProviders: ['COGNITO', 'Facebook', 'LoginWithAmazon'], + SupportedIdentityProviders: ['COGNITO', 'Facebook', 'LoginWithAmazon', 'Google'], }); }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts new file mode 100644 index 0000000000000..41700abe1c92d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts @@ -0,0 +1,101 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderGoogle } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('google', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Google', + ProviderType: 'Google', + ProviderDetails: { + client_id: 'google-client-id', + client_secret: 'google-client-secret', + authorize_scopes: 'profile', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + scopes: ['scope1', 'scope2'], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Google', + ProviderType: 'Google', + ProviderDetails: { + client_id: 'google-client-id', + client_secret: 'google-client-secret', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + attributeMapping: { + givenName: ProviderAttribute.GOOGLE_NAME, + address: ProviderAttribute.other('google-address'), + custom: { + customAttr1: ProviderAttribute.GOOGLE_EMAIL, + customAttr2: ProviderAttribute.other('google-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'google-address', + customAttr1: 'email', + customAttr2: 'google-custom-attr', + }, + }); + }); + }); +}); \ No newline at end of file