Serverless User Identity and Access Management with AWS Cognito and DynamoDB
This article will enable you to deploy a fully functional User API to your AWS account and freely use it for your own intents and purposes.
A cornerstone in enterprise software is secure user management, authentication and authorisation. We need systems that can:
- Register new users
- Store user profile data
- Authenticate (verify a user's identity)
- Authorise (grant access to a resource)
- Token management and validation (JWT Bearer tokens)
- API support for common user flows: Account activation, password reset etc
- Manage static assets: Avatars, icons, profile images (AWS S3)
In this build I'm using Cognito for authentication and DynamoDB to stash user profile data. I link the two using a foreign key scheme. I'm also using s3 to host a bunch of static assets (icons, badges and stuff included in the src).
User model sample
{
"id": "6b09fb7c-518f-48df-9e2d-d406b5cc6965",
"name": "cortney_gutkowski67",
"handle": "@CortneyGutkowski67",
"createdAt": "2022-11-24T15:11:53.829Z",
"lastLoginAt": "2022-11-24T15:12:08.098Z",
"email": "deion.gusikowski56@gmail.com",
"role": "USER",
"status": "CONFIRMED",
"sourceSystem": "customer-website",
"profile": {
"profileData": {
"lang": "en_us",
"avatarUrl": "https://s3.ap-southeast-2.amazonaws.com/dev.xxxxxxxxx/avatars/x256/01.png",
"currency": "USD"
},
"badges": [
{
"name": "NEW_MEMBER",
"iconUrl": "https://s3.ap-southeast-2.amazonaws.com/dev.xxxxxxxxx/badges/new-member.svg"
}
],
"data": {}
}
}
Service design
Key concepts:
- Human users authenticate with time restricted Bearer tokens previously obtained via an Oauth2 client credentials flow
- Machine users authenticate with API keys securely stored in AWS Secrets Manager (suitable for BFF pattern)
- Public routes exposing user data are secured with Cognito Authorisers where JWT validation happens prior to the request being received in the Lambda handler
Serverless.yaml
Below, a stripped down configuration file, outlining the most important resources and a single Lambda function protected by a Cognito Authoriser.
service: ${file(./package.json):name}
frameworkVersion: "3"
provider:
name: aws
region: ap-southeast-2
runtime: nodejs16.x
functions:
getUser:
handler: src/handlers/user/me.handler
events:
- http:
path: public/user/me
method: get
cors: true
authorizer:
name: IdTokenAuthorizer
type: COGNITO_USER_POOLS
arn:
Fn::GetAtt:
- CognitoUserPool
- Arn
claims:
- email
resources:
Resources:
StaticAssetsBucket:
Type: AWS::S3::Bucket
PublicBucketPolicy:
Type: AWS::S3::BucketPolicy
CognitoUserClient:
Type: AWS::Cognito::UserPoolClient
CognitoUserPool:
Type: AWS::Cognito::UserPool
UserTable:
Type: AWS::DynamoDB::Table
This build combines the security of Cognito with the flexibility and low cost of Lambda and DynamoDB.
Route table
Numerous Lambda functions provide API access via API Gateway to common User API features.
It's not complete, but should get you started.
Method | Path | Usage | Route type | ACL |
---|---|---|---|---|
GET |
/public/user/me |
Return user profile data for a valid access token. Useful for rendering a user's account page while letting Cognito implicitly handle the authorization | Public | Bearer token |
POST |
/auth/login |
Exchange credentials for an access token | S2S | API key |
GET |
/auth/token/{tokenType}/verify/{token} |
Verify token. Types = access or id (derived from Cognito) |
S2S | API key |
POST |
/user |
Register new user | S2S | API key |
GET |
/user/id |
Get user by ID | ||
DELETE |
/user/id |
Delete user by ID (hard delete) | S2S | API key |
GET |
/user/name/{name} |
Get user by name (GSI) | S2S | API key |
GET |
/user/email/{email} |
Get user by email (GSI) | S2S | API key |
GET |
/user/handle/{handle} |
Get user by handle e.g @SomePerson (GSI) |
S2S | API key |
GET |
/user/activate/{activationCode} |
Activate user (changes status to UserStatus.CONFIRMED (GSI) |
S2S | API key |
PATCH |
/user/{id}/status/{status} |
Update user status | S2S | API key |
PUT |
/user/{id}/data/update |
Fully update arbitrary user data blob (replace) | S2S | API key |
PATCH |
/user/{id}/data/update |
Partially update arbitrary user data blob (merge) | S2S | API key |
POST |
/user/{id}/badge/{name} |
Issue badge to a user e.g CONVERSATION_STARTER |
S2S | API key |
DELETE |
/user/{id}/badge/{name} |
Revoke user badge | S2S | API key |
Register new user
In below code snippet, I'm establishing a foreign key relationship between the Cognito user and the DynamoDB user document.
To accomplish this, I'm using the Cognito admin command from @AWS-SDK (imports omitted for brevity):
Authentication
Credentials verification and token exchange is handled by the Cognito server-side admin interface.
Retrospective
Could I have built this in a better way? Perhaps.
I'm conscious that I'm combining security and access related features with management of user profile data, sign-up, retrieving profile data etc. The two concepts are related, but I could have taken the authentication functionality and moved into an Auth API and kept the user related functionality in a User API.
This would open up to other possibilities, where multiple User APIs can use a single authentication API, or the authentication API can be utilised in different contexts, now that it is no longer married to the User API.
For anything large and "enterprisy" I would break down these components even further and make parts of it event driven using DynamoDB Stream Events and AWS EventBridge.
Source code on GitHub
Pull requests are welcome.