Source Allies Logo

Sharing Our Passion for Technology

& Continuous Learning

<   Back to Blog

Node Reference - Authentication

Teammates sitting together and looking at code

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

If you have questions or feedback on this series, contact the authors at nodereference@sourceallies.com.