diff --git a/docs/providers/aws/events/cognito-user-pool.md b/docs/providers/aws/events/cognito-user-pool.md index 69d3c60a8..25e453120 100644 --- a/docs/providers/aws/events/cognito-user-pool.md +++ b/docs/providers/aws/events/cognito-user-pool.md @@ -78,6 +78,8 @@ functions: Sometimes you might want to attach Lambda functions to existing Cognito User Pools. In that case you just need to set the `existing` event configuration property to `true`. All the other config parameters can also be used on existing user pools: +**IMPORTANT:** You can only attach 1 existing Cognito User Pool per function. + **NOTE:** Using the `existing` config will add an additional Lambda function and IAM Role to your stack. The Lambda function backs-up the Custom Cognito User Pool Resource which is used to support existing user pools. ```yaml diff --git a/lib/plugins/aws/customResources/resources/cognitoUserPool/handler.js b/lib/plugins/aws/customResources/resources/cognitoUserPool/handler.js index c05dc215f..ba9fdbd8f 100644 --- a/lib/plugins/aws/customResources/resources/cognitoUserPool/handler.js +++ b/lib/plugins/aws/customResources/resources/cognitoUserPool/handler.js @@ -16,7 +16,7 @@ function handler(event, context) { } function create(event, context) { - const { FunctionName, UserPoolName, UserPoolConfig } = event.ResourceProperties; + const { FunctionName, UserPoolName, UserPoolConfigs } = event.ResourceProperties; const { Region, AccountId } = getEnvironment(context); const lambdaArn = getLambdaArn(Region, AccountId, FunctionName); @@ -32,7 +32,7 @@ function create(event, context) { updateConfiguration({ lambdaArn, userPoolName: UserPoolName, - userPoolConfig: UserPoolConfig, + userPoolConfigs: UserPoolConfigs, region: Region, }) ) @@ -41,14 +41,14 @@ function create(event, context) { function update(event, context) { const { Region, AccountId } = getEnvironment(context); - const { FunctionName, UserPoolName, UserPoolConfig } = event.ResourceProperties; + const { FunctionName, UserPoolName, UserPoolConfigs } = event.ResourceProperties; const lambdaArn = getLambdaArn(Region, AccountId, FunctionName); return updateConfiguration({ lambdaArn, userPoolName: UserPoolName, - userPoolConfig: UserPoolConfig, + userPoolConfigs: UserPoolConfigs, region: Region, }); } diff --git a/lib/plugins/aws/customResources/resources/cognitoUserPool/lib/userPool.js b/lib/plugins/aws/customResources/resources/cognitoUserPool/lib/userPool.js index fe4964382..ac68f3f5f 100644 --- a/lib/plugins/aws/customResources/resources/cognitoUserPool/lib/userPool.js +++ b/lib/plugins/aws/customResources/resources/cognitoUserPool/lib/userPool.js @@ -42,7 +42,7 @@ function getConfiguration(config) { } function updateConfiguration(config) { - const { lambdaArn, userPoolConfig, region } = config; + const { lambdaArn, userPoolConfigs, region } = config; const cognito = new CognitoIdentityServiceProvider({ region }); return getConfiguration(config).then(res => { @@ -54,7 +54,9 @@ function updateConfiguration(config) { return accum; }, LambdaConfig); - LambdaConfig[userPoolConfig.Trigger] = lambdaArn; + userPoolConfigs.forEach(poolConfig => { + LambdaConfig[poolConfig.Trigger] = lambdaArn; + }); return cognito.updateUserPool({ UserPoolId, LambdaConfig }).promise(); }); diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index f5ac5d50a..2376e7401 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -485,8 +485,12 @@ module.exports = { )}` ); }, - getCustomResourceCognitoUserPoolResourceLogicalId(functionName, idx) { - return `${this.getNormalizedFunctionName(functionName)}CustomCognitoUserPool${idx}`; + getCustomResourceCognitoUserPoolResourceLogicalId(functionName) { + // NOTE: we have to keep the 1 at the end to ensure backwards compatibility + // previously we've used an index to allow the creation of multiple custom + // Cognito User Pool resources + // we're now using one resource to handle multiple Cognito User Pool event definitions + return `${this.getNormalizedFunctionName(functionName)}CustomCognitoUserPool1`; }, // Event Bridge getCustomResourceEventBridgeHandlerFunctionName() { diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js index de4f6dc66..1bc080082 100644 --- a/lib/plugins/aws/lib/naming.test.js +++ b/lib/plugins/aws/lib/naming.test.js @@ -780,10 +780,9 @@ describe('#naming()', () => { describe('#getCustomResourceCognitoUserPoolResourceLogicalId()', () => { it('should return the logical id of the Cognito User Pool custom resouce', () => { const functionName = 'my-function'; - const index = 1; - expect( - sdk.naming.getCustomResourceCognitoUserPoolResourceLogicalId(functionName, index) - ).to.equal('MyDashfunctionCustomCognitoUserPool1'); + expect(sdk.naming.getCustomResourceCognitoUserPoolResourceLogicalId(functionName)).to.equal( + 'MyDashfunctionCustomCognitoUserPool1' + ); }); }); diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js index d1187bde8..758a4bca4 100644 --- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js +++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.js @@ -108,56 +108,90 @@ class AwsCompileCognitoUserPoolEvents { const { service } = this.serverless; const { provider } = service; const { compiledCloudFormationTemplate } = provider; + const { Resources } = compiledCloudFormationTemplate; const iamRoleStatements = []; + // used to keep track of the custom resources created for each Cognito User Pool + const poolResources = {}; + service.getAllFunctions().forEach(functionName => { + let numEventsForFunc = 0; + let currentPoolName = null; let funcUsesExistingCognitoUserPool = false; const functionObj = service.getFunction(functionName); const FunctionName = functionObj.name; if (functionObj.events) { - functionObj.events.forEach((event, idx) => { + functionObj.events.forEach(event => { if (event.cognitoUserPool && event.cognitoUserPool.existing) { - idx++; + numEventsForFunc++; const { pool, trigger } = event.cognitoUserPool; funcUsesExistingCognitoUserPool = true; + if (!currentPoolName) { + currentPoolName = pool; + } + if (pool !== currentPoolName) { + const errorMessage = [ + 'Only one Cognito User Pool can be configured per function.', + ` In "${FunctionName}" you're attempting to configure "${currentPoolName}" and "${pool}" at the same time.`, + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + const eventFunctionLogicalId = this.provider.naming.getLambdaLogicalId(functionName); const customResourceFunctionLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolHandlerFunctionLogicalId(); - const customCognitoUserPoolResourceLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolResourceLogicalId( - functionName, - idx + const customPoolResourceLogicalId = this.provider.naming.getCustomResourceCognitoUserPoolResourceLogicalId( + functionName ); - const customCognitoUserPool = { - [customCognitoUserPoolResourceLogicalId]: { - Type: 'Custom::CognitoUserPool', - Version: 1.0, - DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId], - Properties: { - ServiceToken: { - 'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'], - }, - FunctionName, - UserPoolName: pool, - UserPoolConfig: { - Trigger: trigger, + // store how often the custom Cognito User Pool resource is used + if (poolResources[pool]) { + poolResources[pool] = _.union(poolResources[pool], [customPoolResourceLogicalId]); + } else { + Object.assign(poolResources, { + [pool]: [customPoolResourceLogicalId], + }); + } + + let customCognitoUserPoolResource; + if (numEventsForFunc === 1) { + customCognitoUserPoolResource = { + [customPoolResourceLogicalId]: { + Type: 'Custom::CognitoUserPool', + Version: 1.0, + DependsOn: [eventFunctionLogicalId, customResourceFunctionLogicalId], + Properties: { + ServiceToken: { + 'Fn::GetAtt': [customResourceFunctionLogicalId, 'Arn'], + }, + FunctionName, + UserPoolName: pool, + UserPoolConfigs: [ + { + Trigger: trigger, + }, + ], }, }, - }, - }; + }; - _.merge(compiledCloudFormationTemplate.Resources, customCognitoUserPool); + iamRoleStatements.push({ + Effect: 'Allow', + Resource: '*', + Action: [ + 'cognito-idp:ListUserPools', + 'cognito-idp:DescribeUserPool', + 'cognito-idp:UpdateUserPool', + ], + }); + } else { + Resources[customPoolResourceLogicalId].Properties.UserPoolConfigs.push({ + Trigger: trigger, + }); + } - iamRoleStatements.push({ - Effect: 'Allow', - Resource: '*', - Action: [ - 'cognito-idp:ListUserPools', - 'cognito-idp:DescribeUserPool', - 'cognito-idp:UpdateUserPool', - ], - }); + _.merge(Resources, customCognitoUserPoolResource); } }); } @@ -171,6 +205,22 @@ class AwsCompileCognitoUserPoolEvents { } }); + // check if we need to add DependsOn clauses in case more than 1 + // custom resources are created for one Cognito User Pool (to avoid race conditions) + if (Object.keys(poolResources).length > 0) { + Object.keys(poolResources).forEach(pool => { + const resources = poolResources[pool]; + if (resources.length > 1) { + resources.forEach((currResourceLogicalId, idx) => { + if (idx > 0) { + const prevResourceLogicalId = resources[idx - 1]; + Resources[currResourceLogicalId].DependsOn.push(prevResourceLogicalId); + } + }); + } + }); + } + if (iamRoleStatements.length) { return addCustomResourceToService.call(this, 'cognitoUserPool', iamRoleStatements); } diff --git a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js index a79d1c979..173ad247f 100644 --- a/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js +++ b/lib/plugins/aws/package/compile/events/cognitoUserPool/index.test.js @@ -368,6 +368,22 @@ describe('AwsCompileCognitoUserPoolEvents', () => { expect(addCustomResourceToServiceStub).to.have.been.calledOnce; expect(addCustomResourceToServiceStub.args[0][1]).to.equal('cognitoUserPool'); + expect(addCustomResourceToServiceStub.args[0][2]).to.deep.equal([ + { + Action: [ + 'cognito-idp:ListUserPools', + 'cognito-idp:DescribeUserPool', + 'cognito-idp:UpdateUserPool', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + Effect: 'Allow', + Resource: 'arn:aws:lambda:*:*:function:first', + }, + ]); expect(Resources.FirstCustomCognitoUserPool1).to.deep.equal({ Type: 'Custom::CognitoUserPool', Version: 1, @@ -378,13 +394,273 @@ describe('AwsCompileCognitoUserPoolEvents', () => { }, FunctionName: 'first', UserPoolName: 'existing-cognito-user-pool', - UserPoolConfig: { - Trigger: 'CustomMessage', - }, + UserPoolConfigs: [ + { + Trigger: 'CustomMessage', + }, + ], }, }); }); }); + + it('should create the necessary resources for a service using multiple event definitions', () => { + awsCompileCognitoUserPoolEvents.serverless.service.functions = { + first: { + name: 'first', + events: [ + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'CustomMessage', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'PreSignUp', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'DefineAuthChallenge', + existing: true, + }, + }, + ], + }, + }; + + return expect( + awsCompileCognitoUserPoolEvents.existingCognitoUserPools() + ).to.be.fulfilled.then(() => { + const { + Resources, + } = awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate; + + expect(addCustomResourceToServiceStub).to.have.been.calledOnce; + expect(addCustomResourceToServiceStub.args[0][1]).to.equal('cognitoUserPool'); + expect(addCustomResourceToServiceStub.args[0][2]).to.deep.equal([ + { + Action: [ + 'cognito-idp:ListUserPools', + 'cognito-idp:DescribeUserPool', + 'cognito-idp:UpdateUserPool', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + Effect: 'Allow', + Resource: 'arn:aws:lambda:*:*:function:first', + }, + ]); + expect(Resources.FirstCustomCognitoUserPool1).to.deep.equal({ + Type: 'Custom::CognitoUserPool', + Version: 1, + DependsOn: ['FirstLambdaFunction', 'CustomDashresourceDashexistingDashcupLambdaFunction'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDashresourceDashexistingDashcupLambdaFunction', 'Arn'], + }, + FunctionName: 'first', + UserPoolName: 'existing-cognito-user-pool', + UserPoolConfigs: [ + { + Trigger: 'CustomMessage', + }, + { + Trigger: 'PreSignUp', + }, + { + Trigger: 'DefineAuthChallenge', + }, + ], + }, + }); + }); + }); + + it('should create DependsOn clauses when one cognito user pool is used in more than 1 custom resources', () => { + awsCompileCognitoUserPoolEvents.serverless.service.functions = { + first: { + name: 'first', + events: [ + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'CustomMessage', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'PreSignUp', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'DefineAuthChallenge', + existing: true, + }, + }, + ], + }, + second: { + name: 'second', + events: [ + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'PostConfirmation', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'PreAuthentication', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'PostAuthentication', + existing: true, + }, + }, + ], + }, + }; + + return expect( + awsCompileCognitoUserPoolEvents.existingCognitoUserPools() + ).to.be.fulfilled.then(() => { + const { + Resources, + } = awsCompileCognitoUserPoolEvents.serverless.service.provider.compiledCloudFormationTemplate; + + expect(addCustomResourceToServiceStub).to.have.been.calledOnce; + expect(addCustomResourceToServiceStub.args[0][1]).to.equal('cognitoUserPool'); + expect(addCustomResourceToServiceStub.args[0][2]).to.deep.equal([ + { + Action: [ + 'cognito-idp:ListUserPools', + 'cognito-idp:DescribeUserPool', + 'cognito-idp:UpdateUserPool', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + Effect: 'Allow', + Resource: 'arn:aws:lambda:*:*:function:first', + }, + { + Action: [ + 'cognito-idp:ListUserPools', + 'cognito-idp:DescribeUserPool', + 'cognito-idp:UpdateUserPool', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: ['lambda:AddPermission', 'lambda:RemovePermission'], + Effect: 'Allow', + Resource: 'arn:aws:lambda:*:*:function:second', + }, + ]); + expect(Object.keys(Resources)).to.have.length(2); + expect(Resources.FirstCustomCognitoUserPool1).to.deep.equal({ + Type: 'Custom::CognitoUserPool', + Version: 1, + DependsOn: ['FirstLambdaFunction', 'CustomDashresourceDashexistingDashcupLambdaFunction'], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDashresourceDashexistingDashcupLambdaFunction', 'Arn'], + }, + FunctionName: 'first', + UserPoolName: 'existing-cognito-user-pool', + UserPoolConfigs: [ + { + Trigger: 'CustomMessage', + }, + { + Trigger: 'PreSignUp', + }, + { + Trigger: 'DefineAuthChallenge', + }, + ], + }, + }); + expect(Resources.SecondCustomCognitoUserPool1).to.deep.equal({ + Type: 'Custom::CognitoUserPool', + Version: 1, + DependsOn: [ + 'SecondLambdaFunction', + 'CustomDashresourceDashexistingDashcupLambdaFunction', + 'FirstCustomCognitoUserPool1', + ], + Properties: { + ServiceToken: { + 'Fn::GetAtt': ['CustomDashresourceDashexistingDashcupLambdaFunction', 'Arn'], + }, + FunctionName: 'second', + UserPoolName: 'existing-cognito-user-pool', + UserPoolConfigs: [ + { + Trigger: 'PostConfirmation', + }, + { + Trigger: 'PreAuthentication', + }, + { + Trigger: 'PostAuthentication', + }, + ], + }, + }); + }); + }); + + it('should throw if more than 1 Cognito User Pool is configured per function', () => { + awsCompileCognitoUserPoolEvents.serverless.service.functions = { + first: { + name: 'first', + events: [ + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool', + trigger: 'CustomMessage', + existing: true, + }, + }, + { + cognitoUserPool: { + pool: 'existing-cognito-user-pool-2', + trigger: 'PreSignUp', + existing: true, + }, + }, + ], + }, + }; + + return expect(() => awsCompileCognitoUserPoolEvents.existingCognitoUserPools()).to.throw( + 'Only one Cognito User Pool' + ); + }); }); describe('#mergeWithCustomResources()', () => { diff --git a/lib/plugins/aws/package/compile/events/s3/index.js b/lib/plugins/aws/package/compile/events/s3/index.js index 7df67c050..04af67ff7 100644 --- a/lib/plugins/aws/package/compile/events/s3/index.js +++ b/lib/plugins/aws/package/compile/events/s3/index.js @@ -208,6 +208,7 @@ class AwsCompileS3Events { service.getAllFunctions().forEach(functionName => { let numEventsForFunc = 0; + let currentBucketName = null; let funcUsesExistingS3Bucket = false; const functionObj = service.getFunction(functionName); const FunctionName = functionObj.name; @@ -221,6 +222,17 @@ class AwsCompileS3Events { const notificationEvent = event.s3.event || 's3:ObjectCreated:*'; funcUsesExistingS3Bucket = true; + if (!currentBucketName) { + currentBucketName = bucket; + } + if (bucket !== currentBucketName) { + const errorMessage = [ + 'Only one S3 Bucket can be configured per function.', + ` In "${FunctionName}" you're attempting to configure "${currentBucketName}" and "${bucket}" at the same time.`, + ].join(''); + throw new this.serverless.classes.Error(errorMessage); + } + rules = _.map(event.s3.rules, rule => { const key = Object.keys(rule)[0]; const value = rule[key]; diff --git a/lib/plugins/aws/package/compile/events/s3/index.test.js b/lib/plugins/aws/package/compile/events/s3/index.test.js index 5fbc9fedf..dce6a20dd 100644 --- a/lib/plugins/aws/package/compile/events/s3/index.test.js +++ b/lib/plugins/aws/package/compile/events/s3/index.test.js @@ -741,5 +741,29 @@ describe('AwsCompileS3Events', () => { }); }); }); + + it('should throw if more than 1 S3 bucket is configured per function', () => { + awsCompileS3Events.serverless.service.functions = { + first: { + name: 'second', + events: [ + { + s3: { + bucket: 'existing-s3-bucket', + existing: true, + }, + }, + { + s3: { + bucket: 'existing-s3-bucket-2', + existing: true, + }, + }, + ], + }, + }; + + return expect(() => awsCompileS3Events.existingS3Buckets()).to.throw('Only one S3 Bucket'); + }); }); }); diff --git a/tests/integration-all/cognito-user-pool/service/core.js b/tests/integration-all/cognito-user-pool/service/core.js index efd9eda23..4de4bf8e9 100644 --- a/tests/integration-all/cognito-user-pool/service/core.js +++ b/tests/integration-all/cognito-user-pool/service/core.js @@ -13,8 +13,8 @@ function basic(event, context, callback) { return callback(null, nextEvent); } -function existing(event, context, callback) { - const functionName = 'existing'; +function existingSimple(event, context, callback) { + const functionName = 'existingSimple'; const nextEvent = Object.assign({}, event); nextEvent.response.autoConfirmUser = true; @@ -22,4 +22,11 @@ function existing(event, context, callback) { return callback(null, nextEvent); } -module.exports = { basic, existing }; +function existingMulti(event, context, callback) { + const functionName = 'existingMulti'; + + log(functionName, JSON.stringify(event)); + return callback(null, event); +} + +module.exports = { basic, existingSimple, existingMulti }; diff --git a/tests/integration-all/cognito-user-pool/service/serverless.yml b/tests/integration-all/cognito-user-pool/service/serverless.yml index 1e1af4494..1f10c0d8b 100644 --- a/tests/integration-all/cognito-user-pool/service/serverless.yml +++ b/tests/integration-all/cognito-user-pool/service/serverless.yml @@ -12,10 +12,22 @@ functions: - cognitoUserPool: pool: CHANGE_TO_UNIQUE_PER_RUN trigger: PreSignUp - existing: - handler: core.existing + existingSimple: + handler: core.existingSimple events: - cognitoUserPool: pool: CHANGE_TO_UNIQUE_PER_RUN trigger: PreSignUp existing: true + # testing if two functions share one cognito user pool with multiple configs + existingMulti: + handler: core.existingMulti + events: + - cognitoUserPool: + pool: CHANGE_TO_UNIQUE_PER_RUN + trigger: PreSignUp + existing: true + - cognitoUserPool: + pool: CHANGE_TO_UNIQUE_PER_RUN + trigger: PreAuthentication + existing: true diff --git a/tests/integration-all/cognito-user-pool/tests.js b/tests/integration-all/cognito-user-pool/tests.js index 1e7319e12..a0cedd3d0 100644 --- a/tests/integration-all/cognito-user-pool/tests.js +++ b/tests/integration-all/cognito-user-pool/tests.js @@ -1,6 +1,7 @@ 'use strict'; const path = require('path'); +const BbPromise = require('bluebird'); const { expect } = require('chai'); const { getTmpDirPath } = require('../../utils/fs'); @@ -9,6 +10,9 @@ const { deleteUserPool, findUserPoolByName, createUser, + createUserPoolClient, + setUserPassword, + initiateAuth, } = require('../../utils/cognito'); const { createTestService, @@ -23,7 +27,8 @@ describe('AWS - Cognito User Pool Integration Test', () => { let stackName; let tmpDirPath; let poolBasicSetup; - let poolExistingSetup; + let poolExistingSimpleSetup; + let poolExistingMultiSetup; const stage = 'dev'; beforeAll(() => { @@ -36,17 +41,23 @@ describe('AWS - Cognito User Pool Integration Test', () => { // Ensure unique user pool names for each test (to avoid collision among concurrent CI runs) config => { poolBasicSetup = `${config.service} CUP Basic`; - poolExistingSetup = `${config.service} CUP Existing`; + poolExistingSimpleSetup = `${config.service} CUP Existing Simple`; + poolExistingMultiSetup = `${config.service} CUP Existing Multi`; config.functions.basic.events[0].cognitoUserPool.pool = poolBasicSetup; - config.functions.existing.events[0].cognitoUserPool.pool = poolExistingSetup; + config.functions.existingSimple.events[0].cognitoUserPool.pool = poolExistingSimpleSetup; + config.functions.existingMulti.events[0].cognitoUserPool.pool = poolExistingMultiSetup; + config.functions.existingMulti.events[1].cognitoUserPool.pool = poolExistingMultiSetup; }, }); serviceName = serverlessConfig.service; stackName = `${serviceName}-${stage}`; - // create an external Cognito User Pool - // NOTE: deployment can only be done once the Cognito User Pool is created - console.info(`Creating Cognito User Pool "${poolExistingSetup}"...`); - return createUserPool(poolExistingSetup).then(() => { + // create external Cognito User Pools + // NOTE: deployment can only be done once the Cognito User Pools are created + console.info('Creating Cognito User Pools'); + return BbPromise.all([ + createUserPool(poolExistingSimpleSetup), + createUserPool(poolExistingMultiSetup), + ]).then(() => { console.info(`Deploying "${stackName}" service...`); deployService(); }); @@ -55,8 +66,11 @@ describe('AWS - Cognito User Pool Integration Test', () => { afterAll(() => { console.info('Removing service...'); removeService(); - console.info(`Deleting Cognito User Pool "${poolExistingSetup}"...`); - return deleteUserPool(poolExistingSetup); + console.info('Deleting Cognito User Pools'); + return BbPromise.all([ + deleteUserPool(poolExistingSimpleSetup), + deleteUserPool(poolExistingMultiSetup), + ]); }); describe('Basic Setup', () => { @@ -80,22 +94,53 @@ describe('AWS - Cognito User Pool Integration Test', () => { }); describe('Existing Setup', () => { - it('should invoke function when a user is created', () => { - let userPoolId; - const functionName = 'existing'; - const markers = getMarkers(functionName); + describe('single function / single pool setup', () => { + it('should invoke function when a user is created', () => { + let userPoolId; + const functionName = 'existingSimple'; + const markers = getMarkers(functionName); - return findUserPoolByName(poolExistingSetup) - .then(pool => { - userPoolId = pool.Id; - return createUser(userPoolId, 'janedoe', '!!!wAsD123456wAsD!!!'); - }) - .then(() => waitForFunctionLogs(functionName, markers.start, markers.end)) - .then(logs => { - expect(logs).to.include(`"userPoolId":"${userPoolId}"`); - expect(logs).to.include('"userName":"janedoe"'); - expect(logs).to.include('"triggerSource":"PreSignUp_AdminCreateUser"'); - }); + return findUserPoolByName(poolExistingSimpleSetup) + .then(pool => { + userPoolId = pool.Id; + return createUser(userPoolId, 'janedoe', '!!!wAsD123456wAsD!!!'); + }) + .then(() => waitForFunctionLogs(functionName, markers.start, markers.end)) + .then(logs => { + expect(logs).to.include(`"userPoolId":"${userPoolId}"`); + expect(logs).to.include('"userName":"janedoe"'); + expect(logs).to.include('"triggerSource":"PreSignUp_AdminCreateUser"'); + }); + }); + }); + + describe('single function / multi pool setup', () => { + it('should invoke function when a user inits auth after being created', () => { + let userPoolId; + let clientId; + const functionName = 'existingMulti'; + const markers = getMarkers(functionName); + const username = 'janedoe'; + const password = '!!!wAsD123456wAsD!!!'; + + return findUserPoolByName(poolExistingMultiSetup) + .then(pool => { + userPoolId = pool.Id; + return createUserPoolClient('myClient', userPoolId).then(client => { + clientId = client.UserPoolClient.ClientId; + return createUser(userPoolId, username, password) + .then(() => setUserPassword(userPoolId, username, password)) + .then(() => initiateAuth(clientId, username, password)); + }); + }) + .then(() => waitForFunctionLogs(functionName, markers.start, markers.end)) + .then(logs => { + expect(logs).to.include(`"userPoolId":"${userPoolId}"`); + expect(logs).to.include(`"userName":"${username}"`); + expect(logs).to.include('"triggerSource":"PreSignUp_AdminCreateUser"'); + expect(logs).to.include('"triggerSource":"PreAuthentication_Authentication"'); + }); + }); }); }); }); diff --git a/tests/utils/cognito/index.js b/tests/utils/cognito/index.js index a4b97b017..eb0e9a08d 100644 --- a/tests/utils/cognito/index.js +++ b/tests/utils/cognito/index.js @@ -9,6 +9,17 @@ function createUserPool(name) { return cognito.createUserPool({ PoolName: name }).promise(); } +function createUserPoolClient(name, userPoolId) { + const cognito = new AWS.CognitoIdentityServiceProvider({ region }); + + const params = { + ClientName: name, + UserPoolId: userPoolId, + ExplicitAuthFlows: ['USER_PASSWORD_AUTH'], + }; + return cognito.createUserPoolClient(params).promise(); +} + function deleteUserPool(name) { const cognito = new AWS.CognitoIdentityServiceProvider({ region }); @@ -53,9 +64,38 @@ function createUser(userPoolId, username, password) { return cognito.adminCreateUser(params).promise(); } +function setUserPassword(userPoolId, username, password) { + const cognito = new AWS.CognitoIdentityServiceProvider({ region }); + + const params = { + UserPoolId: userPoolId, + Username: username, + Password: password, + Permanent: true, + }; + return cognito.adminSetUserPassword(params).promise(); +} + +function initiateAuth(clientId, username, password) { + const cognito = new AWS.CognitoIdentityServiceProvider({ region }); + + const params = { + ClientId: clientId, + AuthFlow: 'USER_PASSWORD_AUTH', + AuthParameters: { + USERNAME: username, + PASSWORD: password, + }, + }; + return cognito.initiateAuth(params).promise(); +} + module.exports = { createUserPool: persistentRequest.bind(this, createUserPool), deleteUserPool: persistentRequest.bind(this, deleteUserPool), findUserPoolByName: persistentRequest.bind(this, findUserPoolByName), + createUserPoolClient: persistentRequest.bind(this, createUserPoolClient), createUser: persistentRequest.bind(this, createUser), + setUserPassword: persistentRequest.bind(this, setUserPassword), + initiateAuth: persistentRequest.bind(this, initiateAuth), };