Imagine a customer-facing application where users will log into your web or mobile application. As such, you will expose your APIs through API Gateway with upstream services. For instance, the user should be allowed to make a GET request to an endpoint but should not be allowed to make a POST request to the same endpoint. As a best practice, you should assign users fine scopes to allow or deny access to your API services.
Let's go through the request flow to understand what happens at each step, as shown in the figure (TODO):
- A user logs into the Identity and access management and acquires a JWT ID token, access token etc.
- A RestAPI request is made, and a bearer token—in this solution, an access token—is passed in the headers.
- API Gateway forwards the request to the LambdaRequestAuthorizer.
- LambdaRequestAuthorizer verifies JWT using the Identity and access management provider.
- LambdaRequestAuthorizer looks up into Amazon DynamoDB the scope based on the custom domain path and method /one/get/ or /one/post
- LambdaRequestAuthorizer return ALLOW or DENY.
- The API Gateway policy engine evaluates the policy
- The request is forwarded to the service.
We should have a DynamoDB table made of scopes. For example:
{
"pk": "GET/one",
"scopes": [
"my-audience.read"
]
}
{
"pk": "POST/two",
"scopes": [
"my-audience.write"
]
}
As usual, there are many articles with one of the best from Alex Brie: https://www.alexdebrie.com/posts/lambda-custom-authorizers/#caching-across-multiple-functions.
The key is flexibility. I could return the policy from DynamoDB.
//read-only
{
"principalId": "my-username",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": [
"arn:aws:execute-api:us-east-1:123456789012:qsxrty/test/GET/",
"arn:aws:execute-api:us-east-1:123456789012:qsxrty/test/GET/list"
]
}
}
//write only
{
"principalId": "my-username",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": [
"arn:aws:execute-api:us-east-1:123456789012:qsxrty/test/POST/",
"arn:aws:execute-api:us-east-1:123456789012:qsxrty/test/PATCH/"
"arn:aws:execute-api:us-east-1:123456789012:qsxrty/test/PUT/"
]
}
}
Or I can build up something completely custom like in this example. It all depends.
In theory, if you have an API with different verbs:
- GET /
- POST /
- PATCH /
I could support multiple roles per user.
.
├── API 1 (api-one)/
├── API 2 (api-two)/
├── Shared code (shared)/
├── LambdaRequestAuthorizer (jwt)/
└── cargo.toml
From the root, you can run either:
cargo test
or
make unit-tests
- Create an AWS account if you do not already have one and log in. The IAM user you use must have sufficient permissions to make AWS service calls and manage AWS resources.
- AWS CLI installed and configured
- Git Installed
- AWS Serverless Application Model (AWS SAM) installed
- Rust 1.64.0 or higher
- cargo-zigbuild and Zig for cross-compilation
- nextest Nextest is a next-generation test runner for Rust.
ASSUMPTION:
-
DynamoDB Scope table is present
-
Custom domain certificate is present
-
Create a Route 53 alias record that routes traffic to the custom domain
-
Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
git clone https://github.com/ymwjbxxq/fine-grained-authorization-apigw-lambda-dynamodb.git
-
Change the directory to the pattern directory:
cd fine-grained-authorization-apigw-lambda-dynamodb.git
-
Deploy the LambdaRequestAuthorizer:
cd jwt make build make deploy
-
Deploy the api-one:
cd api-one make build make deploy
-
Deploy the api-two:
cd api-two make build make deploy
-
Deploy the custom domain:
sam deploy --guided --no-fail-on-empty-changeset --no-confirm-changeset --stack-name myproject-customdomain --template-file ./custom-domain.yml
-
During the prompts:
- Enter a stack name
- Enter the desired AWS Region
- Allow SAM CLI to create IAM roles with the required permissions.
Once you have run
sam deploy -guided
mode and saved arguments to a configuration file (samconfig.toml), you can usesam deploy
in future to use these defaults. -
Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs used for testing.
Once deployed, you should have:
- 1 Custom domain - configured with two paths/one/ and /two/ that are pointing to the relative APIGW
- 2 APIGW one-api and two-api with only GET method pointing to Lambda Function
- 3 Lambda functions - JWT to lookup into DynamoDB and the handler associated to one-api and two-api
ASSUMPTION:
- DynamoDB Scope table is present
- Custom domain certificate is present
- Create a Route 53 alias record that routes traffic to the custom domain
Call the custom domain, passing your JWT token in the Authorization header if everything is in place. https://{route_53_record}/one/
You can run either:
make delete
Or:
aws cloudformation delete-stack --stack-name STACK_NAME