Avoid Circular References in Post Confirmation Triggers 🌱

This is a growing idea. It might evolve into a full article, or it might stay as a helpful snippet. Either way, I hope you find it useful!

Avoid Circular References in AWS PostConfirmation Triggers 🌱

Problem

Todo: Figure out a way to explain this without melting the reader's brain.

Link to adding a user to a group with a post confirmation trigger: here

Solution

Use AppSync JavaScript resolvers

On the AppSync API

import { defineSchema, defineData } from '@aws-amplify/backend'

// Let Amplify create the table and all the CRUDL operations for us
Business: a.model({
	name: a.string().required(),
	email: a.email().required(),
})
//This is what gets called in a post confirmation trigger (after the user is  successfully created)
createBusinessInGroup: a
			.mutation() // mark this as a custom mutation
			.arguments({
				email: a.email().required(),
				groupName: a.string().required(),
			})
			.returns(a.boolean())
			.handler([
        // We know there's a user, so add them to the gruop
				a.handler.custom({
					dataSource: 'cognitoDS',
					entry: './addUserToGroup.js',
				}),
        // Next, add them to the database
				a.handler.custom({
					dataSource: 'businessTableDS',
					entry: './createBusiness.js',
				}),
			])
			.authorization((allow) => [allow.group('NONE')]), // Don't let this be called by anyone by specifying a group that doesn't exist

export type Schema = ClientSchema<typeof schema>

export const data = defineData({
	name: 'addUserToGroupAndDatabaseAPI',
	schema,
	authorizationModes: {
		defaultAuthorizationMode: 'userPool',
	},
	logging: { //enale logging to see the errors and data in cloudwatch
		fieldLogLevel: 'all',
	}
})

Creating the Resolver to add a user to a group

// addUserToGroup.js
//docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminAddUserToGroup.html
import { util } from '@aws-appsync/utils'
export function request(ctx) {
  return {
    resourcePath: '/',
    method: 'POST',
    params: {
      headers: {
        'content-type': 'application/x-amz-json-1.1',
        'x-amz-target': 'AWSCognitoIdentityProviderService.AdminAddUserToGroup',
      },
      body: {
        GroupName: ctx.args.groupName,
        Username: ctx.args.email,
        UserPoolId: ctx.env.COGNITO_USER_POOL_ID,
      },
    },
  }
}

export function response(ctx) {
  console.log('the context', ctx)
  if (ctx.error) {
    util.error(ctx.error.message, ctx.error.type)
  }

  return { created: true } // This let's the next step in the pipeline know that this step was successful
}

Creating the Resolver to add a user to a database

// createBusiness.js
import { util, CognitoIdentity } from '@aws-appsync/utils'
import * as ddb from '@aws-appsync/utils/dynamodb'

export function request(ctx) {
  console.log('the ctx', ctx)
  if (!ctx.prev.created) {
    //error out if the previous step failed
    util.error('Business was not created', 'BusinessNotFound')
  }

  const identity = ctx.identity as CognitoIdentity
  const id = ctx.args.id

  const now = util.time.nowISO8601()
  const item = {
    __typename: 'Business',
    id: util.autoId(),
    owner: `${identity.sub}::${identity.sub}`, //https://docs.amplify.aws/react/build-a-backend/data/customize-authz/per-user-per-owner-data-access/
    email: ctx.args.email,
    name: ctx.args.name,
    createdAt: now,
    updatedAt: now,
  }
  console.log('the full item', item)

  return ddb.put({
    key: { id },
    item,
  })
}

export function response(ctx) {
  if (ctx.error) {
    util.error(ctx.error.message, ctx.error.type)
  }
  console.log('the ctx', ctx)
  console.log('the result', ctx.result)
  return ctx.result
}

Adding the Permissions for the AppSync Data Source

//backend.ts (similar to what you'd also do in CDK)
//Note that this is creating a new data source, not using the one that Amplify created for us, which is fine.
const businessTableDS = backend.data.addDynamoDbDataSource(
  'businessTableDS',
  backend.data.resources.tables['Business'],
)

businessTableDS.grantPrincipal.addToPrincipalPolicy(
  new PolicyStatement({
    actions: ['dynamodb:PutItem'],
    resources: [backend.data.resources.tables['Business'].tableArn],
  }),
)

Adding the Permissions for the Cognito Data Source

//Create an HTTP data source for the Cognito API
const cognitoDS = backend.data.addHttpDataSource(
	'cognitoDS',
	'https://cognito-idp.us-east-1.amazonaws.com', //TODO: make this region dynamic
	{
		authorizationConfig: {
			signingRegion: 'us-east-1', //TODO: make this region dynamic
			signingServiceName: 'cognito-idp',
		},
	}
)

backend.data.resources.cfnResources.cfnGraphqlApi.environmentVariables = {
	COGNITO_USER_POOL_ID: backend.auth.resources.userPool.userPoolId,
}

//allow the datasource to call the createAdminUser operation
cognitoDS.grantPrincipal.addToPrincipalPolicy(
	new PolicyStatement({
		actions: ['cognito-idp:AdminAddUserToGroup'],
		resources: [backend.auth.resources.userPool.userPoolArn],
	})

Testing

Setup Amplify in a Lambda function, assign the trigger and call the mutation

Same stuff as shown in the previous article