A simple template to develop, publish, and test a simple .NET service backend on AWS, with serverless Lambda functions, supported by DynamoDb, behind an API Gateway, and protected by Cognito auth. 🚀
Hopefully this will accelerate some early prototyping for some of my projects, and perhaps you'll find it useful as a reference too.
The rocket ship design...
Included in this template:
- SAM template
- Sample functions (.NET 6 Lambdas)
- Endpoint tests (MSTest)
- API Gateway configuration
- CORS header support
- Database (DynamoDb)
- Auth (Cognito)
Not included in this template:
- Web and mobile client apps
AWS should come with a warning - if you exceed the free tier, you'll be billed. There's no easy way to set a cap on consumption, so keep an eye on your usage.
This design shouldn't be too hard on the budget during development and prototyping. Here's a quick breakdown of each AWS service used, and their free tiers:
Service | Purpose | Free tier | Free limit |
---|---|---|---|
CloudFormation | Deployment stacks | Always free | 1k handler operations pcm per account |
DynamoDb | Data storage | Always free | 25Gb storage; 200m requests pcm |
Lambda | Serverless functions | Always free | 1m requests pcm; 3.2m secs compute pcm |
API Gateway | API endpoints | 12 months | 1m API calls pcm |
Cognito | Authentication | Always free | 50k active users pcm |
- AWS free tiers and specifics for each service: https://aws.amazon.com/free/
This template assume you're working on a Mac with Homebrew installed and bash
available. If not, very little should be different - but you'll need alternative commands for your OS to install the tools.
brew install awscli
brew install aws-sam-cli
brew install --cask aws-vault
brew install --cask dotnet-sdk
- VSCode AWS Toolkit extension
- Create an IAM user for the aws cli and SAM to use (eg.
administrator
)- Choose access keys, rather than username/password
- Get the credentials for the user (access key id, secret access key)
- Grant the new user administrative permissions, eg. the
AdministratorAccess
policy
cat ~/.aws/config
and aws-vault list
to see your currently configured profiles.
- Add the credentials to
aws-vault
, ie.aws-vault add administrator
- Add the profile to
~/.aws/config
, eg.[profile administrator] region=eu-west-2 output=json
deploy-stack-aws.sh -a <profile-name> -s <stack-name> -u <test-user-email>
- build and deploy the application stack to AWSdelete-stack-aws.sh -a <profile-name> -s <stack-name>
- delete a given stack on AWStest-stack.sh -a <profile-name> -s <stack-name>
- run the endpoint tests against a given stack on AWStest-transient-stack.sh -a <profile-name> -u <test-user-email>
- deploy a stack, run the endpoint tests against it, then delete it
In nearly all cases, you must provide an AWS profile available on your machine, and a stack name. The transient stack test script doesn't require a stack name - it makes one up for you. You must provide a test user email address.
On first deployment, your new test user will receive a temporary password by email. This will then be immediately updated to testpass1
. (This is hard coded in: deploy-stack-aws.sh
.)
You should obtain and use the user's AccessToken
as the Authorization: Bearer
heading in calls to the API, eg.
curl -H "Authorization: Bearer <AccessToken>" https://endpoint.uri.etc.
For all subsequent calls where you need a token, you can use the output from auth-scripts/get-user-access-token.sh
, eg.
export ACCESS_TOKEN=$(auth-scripts/get-user-access-token.sh -u <username> -p <password> -ci <cognito-client-id>)
curl -H "Authorization: Bearer $ACCESS_TOKEN" https://endpoint.uri.etc.
You can test the authentication process with the auth redirect pages (in this repository instantiator/auth-redirect-pages if you wish to learn more).
The callback and logout URLs specified in the Cognito User Pool Client in template.yaml
indicate that these auth redirect pages are acceptable endpoints:
-
https://instantiator.dev/auth-redirect-pages/logged_out.html
-
Deploy a stack, eg.
./deploy-stack-aws.sh -a administrator -s rocket -u <test-user-email>
-
Visit the
AuthTestingUrl
specified in the stack's outputs. This will take you to the Auth Redirect pages, and fill the fields for you:NB. For a blank form, visit: Auth Redirect pages
- Domain prefix - This is the name of your stack, and the subdomain part of the various Cognito URLs.
- Region - Leave this as
eu-west-2
unless you're working in a different region. - Client id - this is the value of the
CognitoClientId
output from your stack. - Redirect uri - Leave this as
https://instantiator.dev/auth-redirect-pages/logged_in.html
unless you have set up your Cognito User Pool Client with alternate URIs.
-
Press Log in to initiate a sign in through Cognito.
-
Provide your test user email address.
-
Unless you have changed it, the deploy script will have set the test user's password to:
testpass1
-
If authenticated, you will be redirected to the logged in page - which should show a code you can use to obtain the user's Id, Access, and Refresh tokens at the token endpoint.
The template deploys two functions - one, called the root handler, is responsible for calls to /FunctionOne
. The other, called the notes handler, is responsible for calls to /FunctionOne/notes
.
When developing your application, give some though to which endpoints need authentication and which don't. template.yaml
applies an authorizer to the notes endpoint, but not to the root endpoint or to the OPTIONS
events.
Handler | Endpoint | Method | Auth | Notes |
---|---|---|---|---|
FunctionOne.HandlerRoot:Handle |
/FunctionOne |
GET |
Not required. | Returns "ok". |
FunctionOne.HandlerRoot:Handle |
/FunctionOne |
OPTIONS |
Not required. | Supports a CORS pre-flight request for headers. |
FunctionOne.HandlerNotes:Handle |
/FunctionOne/notes |
GET |
Access token required. | Returns a count of the number of notes in the database table. |
FunctionOne.HandlerNotes:Handle |
/FunctionOne/notes |
POST |
Access token required. | Adds a new note to the database table. |
FunctionOne.HandlerRoot:Handle |
/FunctionOne/notes |
OPTIONS |
Not required. | Supports a CORS pre-flight request for headers. |
The test-stack.sh
and test-transient-stack.sh
scripts will run the endpoint tests from: src/SampleFunctions/EndpointTests
These will exercise Cognito (retrieving user credentials through the aws.cognito.signin.user.admin
OAuth scope), and then provide the access token they received as in the Authorization: Bearer <token>
header to interact with the APIs.
Retrieve an access token for this user:
export ACCESS_TOKEN=$(auth-scripts/get-user-access-token.sh -u <test-user-email> -p <password> -ci <client-id>)
POST to the notes endpoint, to add a new note to the table:
curl -H "Authorization: Bearer $ACCESS_TOKEN" -X POST https://endpoint.uri.etc/staging/FunctionOne/notes
GET from the notes endpoint, to see how many notes there are:
curl -H "Authorization: Bearer $ACCESS_TOKEN" https://endpoint.uri.etc/staging/FunctionOne/notes
To access the APIs from a browser, additional CORS headers are provided.
- Cross-origin requests are rejected by browsers if CORS headers aren't available (or don't indicate support for the page making the calls).
- CORS headers are also provided for
4XX
(unauthorised) rejections from the Cognito authorizer, and5XX
(server-side) errors from lambda executions.
CORS headers can be generated by the lambda methods themselves, and by Gateway for the 4XX
and 5XX
errors.
Browsers make a pre-flight check using the OPTIONS
method before calling the actual method (eg. GET
/ POST
etc.) for an API call. An additional endpoint is provided for the OPTIONS
HTTP method which should also provide the CORS headers. This is illustrated in template.yaml
and the function code provided.
The code in this template assume that the source of requests comes from https://localhost:5001
(Coincidentally, 5001
is the port that Blazor web assembly apps are hosted on your local machine by default.)
If you use a browser-based client, pay careful attention to the CORS configuration. You'll need to alter the Access-Control-Allow-Origin
header found in each function and template.yaml
to match the URL of the page that makes the call.
- CORS headers in
template.yaml
- CORS headers provided by each function Handler in the response.
Finished? Tidy up so that resources don't accidentally incur costs. Particularly as, by default, some of your endpoints aren't protected by Cognito.
./delete-stack-aws.sh -a <aws-profile> -s <stack-name>
NB. There is a known issue that prevents stack deletion from completing. You can complete deletion in the AWS console.
If scopes are not applied to an API Gateway function's event source, the authorizer will accept the user's id token by default.
If scopes are applied, then the authorizer will accept the user's access token instead.
NB. scopes are applied in template.yaml
- which is why these functions require the access token.
To manage the keychain for aws-vault, you'll want to import it into the Keychain Access application:
- File / Import Items...
- Keychains are located in:
~/Library/Keychains
- Import this file:
aws-vault.keychain-db
Client apps are assumed to be simple (perhaps a SPA), and can be hosted anywhere.
I've used Blazor webasm in the past to build a simple responsive app quickly, and can recommend it - but you may already have a solution (perhaps a trendy javascript or TS framework) in mind.
Where using a browser-based client application, pay careful attention to the CORS configuration. You'll need to alter the Access-Control-Allow-Origin
header found in each function and template.yaml
to match the URL of the page that makes the call.
This template was originally proposed with LocalStack in mind. It's a nice implementation of the AWS APIs that allows local deployment.
However, the Community tier for LocalStack does not support API Gateway, and the Pro tier is slightly too costly to develop the framework against. I've abandoned it for now.
With thanks to...
- Nathaniel Beckstead, for: How to add Cognito to your AWS SAM app
- Jeff Adams, ten mile square, for: AWS SAM API with Cognito
Add testing support, CORS, and simplify the provided lambda functions:
- Update deployment scripts to set the test user password directly
- Add tests, and testing scripts for named stacks and transient stacks
- Apply OAuth scopes to API endpoints in
template.yaml
- Add CORS headers to 4XX and 5XX responses in
template.yaml
- Create an
OPTIONS
lambda handler to provide CORS headers - Provide CORS headers in all other handler responses, too
Add documentation and support for authentication:
- Add authentication URLs to
template.yaml
outputs. - Document manual authentication testing with the auth redirect pages.