Skip to content

Instantly share code, notes, and snippets.

@vdelacou
Last active January 30, 2024 23:54
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save vdelacou/01c6ca030387eeb11b35ff5186195b2f to your computer and use it in GitHub Desktop.
Save vdelacou/01c6ca030387eeb11b35ff5186195b2f to your computer and use it in GitHub Desktop.
Create Multi Tenant AWS Amplify For B2B

npm install -g @aws-amplify/cli

npm init private

amplify init

? Enter a name for the project tenant
? Enter a name for the environment develop
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using none
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script start

amplify add auth

 Do you want to use the default authentication and security configuration? Manual configuration
 Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytic
s, and more)
 Please provide a friendly name for your resource that will be used to label this category in the project:
 Please enter a name for your identity pool. 
 Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No
 Do you want to enable 3rd party authentication providers in your identity pool? No
 Please provide a name for your user pool: 
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in? Email
 Do you want to add User Pool Groups? Yes
? Provide a name for your user pool group: ADMIN
? Do you want to add another User Pool Group No
✔ Sort the user pool groups in order of preference · ADMIN
 Do you want to add an admin queries API? No
 Multifactor authentication (MFA) user login options: OFF
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Please specify an email verification subject: Your verification code
 Please specify an email verification message: Your verification code is {####}
 Do you want to override the default password policy for this User Pool? No
 Warning: you will not be able to edit these selections.
 What attributes are required for signing up? Email
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? No
 Do you want to enable any of the following capabilities?
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? No

amplify push

amplify add api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: tenant
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API Yes, I want to make some additional changes.
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API IAM
? Configure conflict detection? No
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? No

Change the graphql to: schema.graphql

FOllow the instructions here: https://medium.com/@vdelacou/how-to-use-typescript-with-aws-amplify-function-d3e271b11d01

npm init private
npm install aws-lambda aws-sdk
npm install --save-dev typescript @types/node @types/aws-lambda eslint@6.8.0 eslint-plugin-flowtype@3 @typescript-eslint/eslint-plugin@2.31.0 eslint-config-airbnb-base@latest eslint eslint-plugin-import @typescript-eslint/parser@2.31.0 eslint-config-airbnb-typescript prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks@2.5.0
curl -o tsconfig.json https://gist.githubusercontent.com/vdelacou/eeb8931e510218dac1464d3cd592722c/raw/b689f79c9dea03accb4327b06a16df99448f37f7/tsconfig.json
curl -o .eslintrc.json https://gist.githubusercontent.com/vdelacou/d6a880059777451a913acd962b8e43a8/raw/.eslintrc.json
curl -o .eslintignore https://gist.githubusercontent.com/vdelacou/58484f1c11af70aaa457f4e5c289e893/raw/.eslintignore

Add the function

amplify add function

? Provide a friendly name for your resource to be used as a label for this category in the project: manageUser
? Provide the AWS Lambda function name: manageUser
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to update permissions granted to this Lambda function to perform on other resources in your project? Yes
? Select the category auth, storage
? Auth has 2 resources in this project. Select the one you would like your Lambda to access 
? Select the operations you want to permit for  create, read, update, delete
? Storage has 3 resources in this project. Select the one you would like your Lambda to access User:@model(appsync)
? Select the operations you want to permit for User:@model(appsync) create, read, update, delete

Copy the function in ts folder below manageUser.ts

amplify add function

? Provide a friendly name for your resource to be used as a label for this category in the project: getCognitoUser
? Provide the AWS Lambda function name: getCognitoUser
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to access other resources created in this project from your Lambda function? Yes
? Select the category auth
? Auth has 2 resources in this project. Select the one you would like your Lambda to access tenant6729c4446729c444
? Select the operations you want to permit for tenant6729c4446729c444 create, read, update, delete

than add in index.ts the getCognitoUser.ts

amplify add function

? Provide a friendly name for your resource to be used as a label for this category in the project: preTokenGeneration
? Provide the AWS Lambda function name: preTokenGeneration
? Choose the function runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
? Do you want to access other resources created in this project from your Lambda function?? Yes
? Select the category storage
? Storage has 3 resources in this project. Select the one you would like your Lambda to access User:@model(appsync)
? Select the operations you want to permit for User:@model(appsync) create, read, update, delete

Copy the function in ts folder below preTokenGeneration.ts

add to root package.json

  "scripts": {
    "amplify:getCognitoUser": "cd amplify/backend/function/getCognitoUser/ts && npm install && npm run tsc && cd -",
    "amplify:manageUser": "cd amplify/backend/function/manageUser/ts && npm install && npm run tsc && cd -",
    "amplify:preTokenGeneration": "cd amplify/backend/function/preTokenGeneration/ts && npm install && npm run tsc && cd -  "
  }

Then amplify push

And then amplify add storage

go to amplify console and choose in policies that only admin can create account

choose in trigger the pretoken function

/* Amplify Params - DO NOT EDIT
AUTH_TENANT_USERPOOLID
ENV
REGION
Amplify Params - DO NOT EDIT */
import { Handler } from 'aws-lambda';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
const { REGION } = process.env;
if (!REGION) {
throw new Error("Function requires environment variable: 'REGION'");
}
const COGNITO_USERPOOL_ID = process.env.AUTH_TENANT_USERPOOLID;
if (!COGNITO_USERPOOL_ID) {
throw new Error("Function requires environment variable: 'AUTH_TENANT_USERPOOLID'");
}
const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider({ region: REGION });
interface GraphqlEvent {
typeName: string /* Filled dynamically based on @function usage location */;
fieldName: string /* Filled dynamically based on @function usage location */;
arguments: {
/* GraphQL field arguments via $ctx.arguments */
};
identity: {
/*
AppSync identity object via $ctx.identity
https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#aws-appsync-resolver-context-reference-identity
*/
};
source: {
/* The object returned by the parent resolver. E.G. if resolving field 'Post.comments', the source is the Post object. */
userId: string;
};
request: {
/* AppSync request object. Contains things like headers. */
};
prev: {
/* If using the built-in pipeline resolver support, this contains the object returned by the previous function. */
};
}
interface CognitoUser {
/**
* The user email
*/
email?: string;
/**
* The date the user was created.
*/
createDate?: Date;
/**
* The date the user was last modified.
*/
lastModifiedDate?: Date;
/**
* Indicates that the status is enabled.
*/
enabled?: boolean;
}
export const handler: Handler<GraphqlEvent, CognitoUser | void> = async (event: GraphqlEvent): Promise<CognitoUser> => {
// check if it's field for User
if (event.typeName !== 'User') {
throw new Error('NoUserFieldException');
}
// check if it's cognitoUser field
if (event.fieldName !== 'cognitoUser') {
throw new Error('NotcognitoUserFieldException');
}
const params: CognitoIdentityServiceProvider.Types.AdminGetUserRequest = {
UserPoolId: COGNITO_USERPOOL_ID,
Username: event.source.userId,
};
const user = await cognitoIdentityServiceProvider.adminGetUser(params).promise();
if (!user) {
throw new Error('UsernameExistsException');
}
const email = user.UserAttributes && user.UserAttributes.find((value) => value.Name === 'email');
return {
email: email?.Value,
createDate: user.UserCreateDate,
lastModifiedDate: user.UserLastModifiedDate,
enabled: user.Enabled,
};
};
/* Amplify Params - DO NOT EDIT
API_TENANT_GRAPHQLAPIIDOUTPUT
API_TENANT_USERTABLE_ARN
API_TENANT_USERTABLE_NAME
AUTH_TENANT_USERPOOLID
ENV
REGION
Amplify Params - DO NOT EDIT */
import { Handler } from 'aws-lambda';
import { AWSError, CognitoIdentityServiceProvider, DynamoDB } from 'aws-sdk';
import { DeleteItemInput, UpdateItemInput } from 'aws-sdk/clients/dynamodb';
import { PromiseResult } from 'aws-sdk/lib/request';
const { REGION } = process.env;
if (!REGION) {
throw new Error("Function requires environment variable: 'REGION'");
}
const USERPOOL_ID = process.env.AUTH_TENANT_USERPOOLID;
if (!USERPOOL_ID) {
throw new Error("Function requires environment variable: 'AUTH_TENANT_USERPOOLID'");
}
const USERTABLE_NAME = process.env.API_TENANT_USERTABLE_NAME;
if (!USERTABLE_NAME) {
throw new Error("Function requires environment variable: 'API_TENANT_USERTABLE_NAME'");
}
interface GraphqlEvent {
typeName: string /* Filled dynamically based on @function usage location */;
fieldName: string /* Filled dynamically based on @function usage location */;
arguments: {
/* GraphQL field arguments via $ctx.arguments */
input: {
userId?: string;
email?: string;
tenantId?: string;
enabled?: boolean;
};
};
identity: {
/*
AppSync identity object via $ctx.identity
https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#aws-appsync-resolver-context-reference-identity
*/
};
source: {
/* The object returned by the parent resolver. E.G. if resolving field 'Post.comments', the source is the Post object. */
};
request: {
/* AppSync request object. Contains things like headers. */
};
prev: {
/* If using the built-in pipeline resolver support, this contains the object returned by the previous function. */
};
}
interface User {
userId: string;
tenantId: string;
}
const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider({ region: REGION });
const dynamoDB = new DynamoDB({ region: REGION });
export const createUser = async (email: string, tenantId: string): Promise<User> => {
try {
const params: CognitoIdentityServiceProvider.Types.AdminCreateUserRequest = {
UserPoolId: USERPOOL_ID,
Username: email,
};
const request: PromiseResult<CognitoIdentityServiceProvider.AdminCreateUserResponse, AWSError> = await cognitoIdentityServiceProvider.adminCreateUser(params).promise();
const response = request.$response.data as CognitoIdentityServiceProvider.AdminCreateUserResponse;
if (!response.User || !response.User.Username) {
throw new Error('UserNotCreated');
}
const now = new Date();
const putItemInput = {
TableName: USERTABLE_NAME,
Item: {
userId: { S: response.User.Username },
tenantId: { S: tenantId },
__typename: { S: 'User' },
createdAt: { S: `${now.toISOString()}` },
updatedAt: { S: `${now.toISOString()}` },
},
};
try {
await dynamoDB.putItem(putItemInput).promise();
} catch (errorDynamoDB) {
// if cannot saved the user, we delete it
await cognitoIdentityServiceProvider
.adminDeleteUser({
UserPoolId: USERPOOL_ID,
Username: response.User.Username,
})
.promise();
throw errorDynamoDB;
}
return {
userId: response.User.Username,
tenantId,
};
} catch (error) {
// see possible errors here: https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminCreateUser.html
if (error.code === 'UsernameExistsException') {
// can send back the error code you want
throw new Error('UsernameExistsException');
}
throw error;
}
};
export const updateUser = async (userId: string, enabled: boolean): Promise<User> => {
const getItemInput = {
TableName: USERTABLE_NAME,
Key: {
id: {
S: userId,
},
},
};
const User = await dynamoDB.getItem(getItemInput).promise();
if (!User.Item) {
throw new Error('UserNotFoundException');
}
// enabled or disabled the user
const params = {
UserPoolId: USERPOOL_ID,
Username: userId,
};
if (enabled === true) {
await cognitoIdentityServiceProvider.adminEnableUser(params).promise();
}
if (enabled === false) {
await cognitoIdentityServiceProvider.adminDisableUser(params).promise();
}
const now = new Date();
const updateItemInput: UpdateItemInput = {
TableName: USERTABLE_NAME,
Key: {
id: {
S: userId,
},
},
ExpressionAttributeNames: {
'#updatedAt': 'updatedAt',
},
UpdateExpression: '#updatedAt = :updatedAt',
ExpressionAttributeValues: {
':updatedAt': { S: `${now.toISOString()}` },
},
};
await dynamoDB.updateItem(updateItemInput).promise();
const tenantId = User.Item?.tenantId.S;
if (!tenantId) {
throw new Error('TenantNotFoundException');
}
return {
userId,
tenantId,
};
};
export const deleteUser = async (userId: string): Promise<User> => {
const getItemInput = {
TableName: USERTABLE_NAME,
Key: {
id: {
S: userId,
},
},
};
const User = await dynamoDB.getItem(getItemInput).promise();
if (!User.Item) {
throw new Error('UserNotFoundException');
}
const params: CognitoIdentityServiceProvider.Types.AdminCreateUserRequest = {
UserPoolId: USERPOOL_ID,
Username: userId,
};
await cognitoIdentityServiceProvider.adminDeleteUser(params).promise();
const deleteItemInput: DeleteItemInput = {
TableName: USERTABLE_NAME,
Key: {
id: {
S: userId,
},
},
};
await dynamoDB.deleteItem(deleteItemInput).promise();
const tenantId = User.Item?.tenantId.S;
if (!tenantId) {
throw new Error('TenantNotFoundException');
}
return {
userId,
tenantId,
};
};
export const handler: Handler<GraphqlEvent, User | void> = async (event: GraphqlEvent): Promise<User> => {
// check if it's mutation
if (event.typeName !== 'Mutation') {
throw new Error('NotMutationException');
}
switch (event.fieldName) {
case 'createUser': {
const { email, tenantId } = event.arguments.input;
if (!email) {
throw new Error('NoEmailException');
}
if (!tenantId) {
throw new Error('NoTenantIdException');
}
return createUser(email, tenantId);
}
case 'updateUser': {
const { userId, enabled } = event.arguments.input;
if (!userId) {
throw new Error('NoUserIdException');
}
if (!enabled) {
throw new Error('NoEnabledException');
}
return updateUser(userId, enabled);
}
case 'deleteUser': {
const { userId } = event.arguments.input;
if (!userId) {
throw new Error('NoUserIdException');
}
return deleteUser(userId);
}
default:
throw new Error('FieldNameNotExistsException');
}
};
/* Amplify Params - DO NOT EDIT
API_TENANT_GRAPHQLAPIIDOUTPUT
API_TENANT_USERTABLE_ARN
API_TENANT_USERTABLE_NAME
ENV
REGION
Amplify Params - DO NOT EDIT */
import { Callback, CognitoUserPoolTriggerEvent, CognitoUserPoolTriggerHandler, Context } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
const { REGION } = process.env;
if (!REGION) {
throw new Error("Function requires environment variable: 'REGION'");
}
const USERTABLE_NAME = process.env.API_TENANT_USERTABLE_NAME;
if (!USERTABLE_NAME) {
throw new Error("Function requires environment variable: 'API_TENANT_USERTABLE_NAME'");
}
const dynamoDB = new DynamoDB({ region: REGION });
export const handler: CognitoUserPoolTriggerHandler = async (event: CognitoUserPoolTriggerEvent, _context: Context, callback: Callback<CognitoUserPoolTriggerEvent>) => {
const userId = event.userName;
if (!userId) {
throw new Error("Function requires to receive in event the username: 'event.userName'");
}
let claimsToAddOrOverride = {};
const getItemInput = {
TableName: USERTABLE_NAME,
Key: {
id: {
S: userId,
},
},
};
const User = await dynamoDB.getItem(getItemInput).promise();
if (User.Item) {
claimsToAddOrOverride = {
...claimsToAddOrOverride,
tenantId: User.Item.tenantId.S,
};
}
// add the claims to override
Object.assign(event.response, {
claimsOverrideDetails: {
claimsToAddOrOverride,
},
});
callback(null, event);
};
# Tenant of our app
type Tenant
@model
@auth(
rules: [
# Allow the ADMIN to do all
{ allow: groups, groups: ["ADMIN"] }
]
) {
id: ID!
name: String!
logo: S3Object!
}
# User who can access to the data which belongs to a tenant
type User
@model(mutations: null, subscriptions: null)
@key(fields: ["userId"])
@key(name: "ByTenant", fields: ["tenantId"], queryField: "getUserByTenant")
@auth(
rules: [
# Allow the ADMIN to do all
{ allow: groups, groups: ["ADMIN"] }
]
) {
userId: String! # The cognito user Id
cognitoUser: CognitoUser @function(name: "getCognitoUser-${env}")
tenantId: ID!
}
type Pokemon
@model
@key(name: "ByTenant", fields: ["tenantId"], queryField: "getPokemonByTenant")
@auth(
rules: [
# Allow the ADMIN to do all
{ allow: groups, groups: ["ADMIN"] }
# Allow the store user to read only the pokemon for the tenant it belongs to
{ allow: owner, ownerField: "tenantId", identityClaim: "tenantId" }
]
) {
id: ID!
name: String!
picture: S3Object!
tenantId: ID!
}
type CognitoUser {
email: String
createDate: AWSDateTime
lastModifiedDate: AWSDateTime
enabled: Boolean
}
type S3Object {
bucket: String!
region: String!
key: String!
protectedIdentityId: String!
}
input CreateUserInput {
email: String!
tenantId: String!
}
input UpdateUserInput {
userId: String!
enabled: Boolean
}
input DeleteUserInput {
userId: String!
}
type Mutation {
createUser(input: CreateUserInput!): User @function(name: "manageUser-${env}") @auth(rules: [{ allow: groups, groups: ["ADMIN"] }])
updateUser(input: UpdateUserInput!): User @function(name: "manageUser-${env}") @auth(rules: [{ allow: groups, groups: ["ADMIN"] }])
deleteUser(input: DeleteUserInput!): User @function(name: "manageUser-${env}") @auth(rules: [{ allow: groups, groups: ["ADMIN"] }])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment