Sharing Our Passion for Technology
& Continuous Learning
Node Reference - Authentication
Prerequisites
This article builds on the prior article: Node Reference - Cognito.
Adding authentication
In order to leverage our new identity provider, we need to add a middleware into our Koa pipeline. This middleware will reject requests that do not contain valid tokens. We can accomplish this by using two libraries: koa-jwt{:target="_blank"} to validate the token and jwks-rsa{:target="_blank"} to automatically fetch and cache JWKS documents. You can add both of these libraries to your project with the following command:
npm install --save koa-jwt jwks-rsa
Require them at the top of your server.js
file:
const jwt = require('koa-jwt');
const jwksRsa = require('jwks-rsa');
Then you'll want to inject your Cognito User Pool Id into your Node.js service by adding an Environment
section to your cloudformation.template.yml
under AWS::ECS::TaskDefinition
> ContainerDefinitions
section, and importing the User Pool Id you exported from your cognito.template.yml
stack:
ContainerDefinitions:
- Name: ProductService
PortMappings:
- ContainerPort: 3000
Essential: true
Image: !Ref Image
LogConfiguration:
# This tells ECS to send log output to Cloudwatch. The prefix is required by Fargate so don't remove it.
LogDriver: 'awslogs'
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: ProductService
Environment:
- Name: USER_POOL_ID
Value: !ImportValue 'UserPool::Id'
Returning to you server.js
file, add a helper function to create the authentication middleware. You can inject the JWKS url from your new environment variable by using process.env{:target="_blank"}.
function createAuthMiddleware() {
return jwt({
secret: jwksRsa.koaJwtSecret({
cache: true,
jwksUri: `https://cognito-idp.us-east-1.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`
}),
algorithms: ['RS256']
});
}
Lastly, add the following line above the existing app.use
call in order to ensure authentication happens before processing the request:
app.use(createAuthMiddleware());
In order to test our new setup, start the server and attempt to fetch our hello endpoint:
export USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name Cognito \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' \
--output text)
npm run start
curl --verbose http://localhost:3000/hello
You should receive an HTTP 401{:target="_blank"} response back. Now fetch a token from the Cognito token endpoint (they're only valid for so long so get a new one) and include it in the request:
AUTH_NAME="theproducts"
USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name Cognito \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' \
--output text)
USER_POOL_CLIENT_ID=$(aws cognito-idp list-user-pool-clients \
--user-pool-id "$USER_POOL_ID" \
--max-results 1 \
--query 'UserPoolClients[0].ClientId' --output text)
CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client --user-pool-id "$USER_POOL_ID" --client-id "$USER_POOL_CLIENT_ID" --query 'UserPoolClient.ClientSecret' --output text)
# See https://stackoverflow.com/questions/1955505/parsing-json-with-unix-tools
BEARER_TOKEN=$(curl -s -X POST \
https://${USER_POOL_CLIENT_ID}:${CLIENT_SECRET}@${AUTH_NAME}.auth.us-east-1.amazoncognito.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=client_credentials | \
python -c "import sys, json; print json.load(sys.stdin)['access_token']")
$ curl --verbose http://localhost:3000/hello \
-H "Authorization: Bearer $BEARER_TOKEN"
We have now implemented a best of breed security solution with only a few lines of code. However, there is one remaining loose end: If we check in the above changes, then the AWS ECS Service health check will attempt to hit our /hello endpoint and receive a 401 response. By default, any non-200 response is considered "unhealthy" so it will fail to deploy our service. To remedy this, we are going to exclude the /hello endpoint from authentication by modifying the use(createAuthMiddleware())
line in server.js
as follows:
app.use(createAuthMiddleware()
.unless({path: '/hello'}));
We can now safely deploy our service. Commit and push your changes and our CodePipeline pipeline should deploy your service updates. You can test your changes against your service running in AWS with these commands. The first cURL command should return an HTTP 401{:target="_blank"} response because we're not passing a bearer token. The second cURL command should return an HTTP 404{:target="_blank"} response because authentication succeeds and then our Koa HTTP server responds that the route is not found.
If you didn't set up a public DNS name and SSL certificate during the Node Reference - HTTPS article, substitute your load balancer's address and send the request over HTTP
DOMAIN="theproducts.sourceallies.com"
curl -s -I https://${DOMAIN}/
curl -s -I https://${DOMAIN}/example -H "Authorization: Bearer $BEARER_TOKEN"
See the changes we made here{:target="_blank}.
Table of Contents
- Introduction
- Unit Testing
- Koa
- Docker
- Cloudformation
- CodePipeline
- Fargate
- Application Load Balancer
- HTTPS/DNS
- Cognito
- Authentication (this post)
- DynamoDB
- Put Product
- Validation
- Smoke Testing
- Monitoring
- List Products
- Get Product
- Patch Product
- History Tracking
- Delete
- Change Events
- Conclusion
If you have questions or feedback on this series, contact the authors at nodereference@sourceallies.com.