Package: pg-cognition
A framework tying Aurora Serverless Postgres together with AWS Cognito and Appsync in a multi-tenant application.The package includes utilities for creating a database schema with the needed tables for users and tenants, creating Necessary Cognito webooks, and resolving Appsync queries.AthenticationAuthentication in Aurora Serveless is done via AWS secrets that contain database credentials. pg-cognition creates users in the database that map to credentials in AWS Secrets Manager to provide authorization
on the database layer instead of requiring it in the application layer. This results in a high level of security due to the fact that an exploit in the application layer will not allow a user to access resources
that they are not granted in the database layer.Database rolesThe basis of authorization is achieved through a scaffolding of database roles and Row Level Security Policies. The following roles dictate what users can do in the users and tenants table in the pg-cognition schema,
which controls authorization.application_admins- Have the keys to the kingdom. They can modify any attribute of any user or tenanttenant_admins- Can modify any attribute of a user (except their database username) that belongs to their tenant and change their tenant’s display nametenant_users- Can modify their own basic infoTenants and schemaspg-cognition creates a multi-tenant system by segregating tenants into their own schemas. This creates a layer of isolation in data without having to create RLS policies for any piece of data that you want secregated.
You can of course create a single tenant and assign all users to that tenant if you wish for your application to behave differently. Users are “locked into” their tenants’s schema through role inheritance.Before creating a tenant you first create a template schema named “tenant_template”. This schema is used as the template for any user you create and contain any database objects that you want, includingtablestriggersfunctionsRLS PoliciesetcWhen a tenant is created this schema is cloned, along with all of its child objects. There also an admin and a tenant role that are created with only permissions on that schema.
These roles inherit the tenant_admins and tenant_users roles (respectively). Fine tuning permissions can be done in three different ways:Modifying Grants for the tenant level roles to apply new permissions only to the users and/or admins in that tenant for only their schemaModifying Grants to the tenant_admins/tenant_users roles, which will grant these permissions to the users/admins of all tenantsApplying RLS to a specific schemaAppsyncThis package includes utilities for creating a Lambda function that can function as an Appsync datasource. This works by grabbing the Cognito identity or IAM arn of the entitiy
who made the call to appsync and then performing the query using their db credentials. Using this datasource makes creating resolvers simple. By default we handle single or batch
invocations, returning the data in the format that Appsync expects from our method. Resolvers simply define a query and a list of parameters that will be passed to the call to Aurora.
No authorization has to happen in the resolver itself and no parsing has to happen in the response template to organize the data into the format that Appsync expects.Features:Batch or single invocations work without any extra configurationDatabase credentials are fetched based on the Cognito user who made the Appsync callAdditional overrides allow IAM roles and IAM Assumed roles to be mapped to specific database credentialsResults are automatically formatted into a list of dictionaries with the column names as keys (or a list of lists in the case of a batch invoke) using aurora-prettyparser packageCognitoIntegration with Cognito is what glues the application layer to the database authorization layer. This starts with implementing Cognito hooks that tie users in the database users table to a cognito user.
Standard user creation workflow:A user is invited to the applicationA row representing that user is created in the users table (with no database credentials and no way to access the app) and is set to an “invited” stateWhen a user attempts to create a cognito user in the application’s user pool the Cognito hooks will prevent a signup if that email address does not exist in the user’s table in the “invited” stateIf the user successfully created in Cognito (eg: was invited and Cognito user creation is successful) then a function can be triggered that willGenerate a random database username and roleAssign that username to the new userCreate the secret for that database user in Secrets ManagerGrant the tenant user/admin role to the new userHooks for loginWhen a user attempts to log into the application we can reject them with a PreAuthentication hook if they do not exist in the application or are not in the “active” status. This allows us to to suspend users
by simply changing their user status in the database. This also blocks attemps to guess/brute force Cognito credentials since the username would not only have to exist in Cognito, but also be active in the application.
Once a user is successfully logged in we can add claims to their JWT to be used in the application using the Cognito module in the PreTokenGeneration hook.
This separation of Cognito users and database users has several advantages.Getting a Cognito login doesn’t guarantee access to the databaseA user can be removed in the application without removing their Cognito userUsers can be suspended in the application without having to touch their Cognito userThe use of Cognito hooks prevents unauthorized logins from existing Cognito users that have been disabled in the applicationRead the full code Documentation HERE