Implementing machine-to-machine authentication for services behind an AWS ALB with OIDC

Yashodha Hettiarachchi
8 min readMay 25, 2023
The LEGO Transformers Optimus Prime set, displayed in robot mode | source

⚠️ Important: Please Note the Series Continuation

🔎 Enforcing machine-to-machine authentication for services behind an AWS ALB with OIDC

This is part of a series. In Part 1, we delved into the possibilities of enforcing machine-to-machine (m2m) authentication using OIDC (OpenID Connect) at a high level when utilizing an AWS ALB. Now, in Part 2, we are ready to take our exploration to the next level and implement some of the solutions we discussed.

Before Jump in

You can find the source code for the scenarios discussed in this series here.

  • To set up the infrastructure for each scenario, we will utilize Terraform. While you don’t need to be an expert in Terraform (which I’m not, btw) to follow the instructions, you will need to have Terraform installed on your machine.
  • From a technical perspective, any Identity Provider (IDP) that complies with OIDC can be used. However, this blog will primarily focus on AWS Cognito as it offers seamless integration with various other AWS services.
  • If you want to deploy the infrastructure in a different region, you can easily update the value of the aws_region variable in the variables-general.auto.tfvars file.
  • Please ensure that you have sufficient permissions to deploy the infrastructure.
  • ……..and, you should have an active AWS account 🤡

Let’s see them in action

To keep things simple, we will focus on a single service: the “order service.” In this case, the order service will simply return a greeting.

Setting up the base model

To deploy the base infrastructure model to your AWS environment, please navigate to the alb/terraform/scenarios/base-model directory in the repository and run the deploy.sh file.

Upon successful execution, your environment should resemble the following setup.

ksdkaSA
Base infrastructure model

You will see several output values on the console, which we will use shortly. ( Displaying the client secret on the console every time you run terraform apply is not a wise idea. For the sake of convenience in this demo, we will do it anyway.)

Terraform outputs — Base model

In your AWS environment, you should now have two VPCs, an internal ALB, a Cognito user pool, an app client with client credentials grant type, and a resource server with a custom scope order:read.

resource server — console view ( Amazon Cognito > User Pools > base-model-user-pool)
OIDC integration — console view (Amazon Cognito > User Pools > base-model-user-pool > base-model-user-pool-client)

To simulate an external service, we have created a separate EC2 instance (external-service-test-instance) in a different VPC and will consume the exposed service from there by running custom Python scripts.

Let’s connect to the external-service-test-instance (you can use EC2 Instance Connect for this) and keep this tab open as we will be going back and forth for testing as we progress.

EC2 connect to instance options — console view

Once connected navigate to the /etc directory. In the /etc directory, you will find two Python scripts named ast_unauth_req.py and ast_auth_req.py, which are located under terraform/modules/external-service/scripts. The code itself is self-explanatory

  • ast_unauth_req.py — initiates a request to a given URL.
  • ast_auth_req.py — obtains an access token from the TOKEN_ENDPOINT (authentication part) by sending the client id, client secret, and the required scope. It then initiates a request to a given URL with the retrieved token in the ‘Authorization’ header field.

Run the following command to export the necessary variables.

export CLIENT_ID={core_cognito_user_pool_client_id}
export TOKEN_ENDPOINT={core_cognito_token_endpoint}
export CLIENT_SECRET={core_cognito_user_pool_client_secret}
export SCOPE=order-resource-server/order:read

replace the values in the curly braces with the output values from the console that were mentioned earlier.

We have successfully deployed our base model. Now, let’s explore the potential approaches we have discussed in action.

Scenario 1: Token verification with AWS APIGW

For the sake of simplicity in this scenario, we will be using a Lambda function. The specific computing structure of our services is not crucial for this example. Our main focus is on using an API Gateway to front our ALB and handle authorization. To keep things simple, we will use an API Gateway HTTP API for the integration.

To proceed, go to the alb/terraform/scenarios/scenario-1 directory and execute the deploy.sh script. This will attach an HTTP API, a VPC link, an order service Lambda, and all the necessary integration configurations to the base infrastructure model. Once the deployment is complete, your environment should look similar to the architecture depicted below.

You should also see a console output that resembles the example below.

We have successfully exposed our order service behind the internal ALB using an HTTP API and enforced strict authentication and authorization measures through AWS Cognito’s OIDC integration with APIGW. Let’s go back to the AWS console and review some important configurations that have been created.

We have created an HTTP listener in the ALB that will forward traffic to the order service if the path pattern is /order-service.

Additionally, we have created an APIGW HTTP API and configured a route with the key /order-service. We also have configured an authorizer and a private integration to our internal ALB.

In the integration section, you can see that we have configured a parameter mapping that will omit the stage portion from the request to the backend targets.

In the authorization section, you can see that we have created a jwt-authorizer which will validate the tokens issued by Cognito. You can find more information on how API Gateway performs token verification here.

Now, let’s head back to the external-service-test-instance and run the following command to export the endpoint URL. Substitute the value within the curly braces with the corresponding output value on the console.

export URL={order_service_invoke_url}

Let’s test the exposed endpoint by executing the following command and check whether we can access the order service.

python3 ast_unauth_req.py

The response should be 401 Unauthorized.

Let’s get a valid token from the Cognito token endpoint and reinitiate the request.

python3 ast_auth_req.py

The above script, when executed, should return a 200 OK response. Essentially, it retrieves a token from the Cognito token endpoint and includes it in the Authorization header. The APIGW then verifies the validity of the token and forwards the request to the internal ALB.

If you’re interested in learning more about the JWTs issued by Cognito, you can paste the token into jwt.io and follow along with this documentation.

To destroy all the infrastructure provisioned in this section, navigate to the terraform/scenarios/scenario-1 directory and run the clean.sh file.

Scenario-2: Token verification, on our own

We have discussed two possibilities here.

  1. Introduce an internal gateway and perform token verification in there
  2. perform token verification in each target

Token verification at the internal gateway

I’ve already implemented token verification leveraging Spring Cloud and Spring Security. You can find the implementation at alb/terraform/scenarios/scenario-3. I am planning to write a separate blog post on this topic, which I will update here very soon.

Token verification at each target

For the sake of simplicity, we will perform the token verification in an AWS Lambda function.

To deploy the following infrastructure on top of the base model we previously deployed, navigate to the alb/terraform/scenarios/scenario-2 directory and execute the deploy.sh script.

We have established a peering connection between the two VPCs to enable internal communication. However, when it comes to authorization, our focus should be on the custom token verification code snippet in the order service Lambda function.

import {CognitoJwtVerifier} from "aws-jwt-verify";
// Get the values from env variables
const userPoolId = process.env.USER_POOL_ID;
const clientId = process.env.CLIENT_ID;
const scope = process.env.SCOPE;
// Verifier that expects valid access tokens:
const verifier = CognitoJwtVerifier.create({
userPoolId: userPoolId,
tokenUse: "access",
clientId: clientId,
scope: scope
});
export async function isAuthorized(headers) {
if (headers.hasOwnProperty('authorization')) {
const accessToken = headers.authorization.split(' ')[1];
return isValidToken(accessToken);
} else {
console.log("No Access token");
return false;
}
}
async function isValidToken(token) {
try {
// Verify the token
const payload = await verifier.verify(
token
);
console.log("Token is valid. Payload:", payload);
return true;
} catch {
console.log("Token not valid!");
return false;
}
}

To validate JWTs from a Node.js app, AWS recommends using the aws-jwt-verify library. If you are using another language, check if there is existing SDK/library support for token verification. The official documentation provides a guided document that fully explains the structure of the JWTs issued by Cognito and how to validate them manually. When using the aws-jwt-verify library, most of the heavy lifting is done for you, and you only need to provide the claims that you want to verify.

Let’s return to the external-service-test-instance and validate the implementation. In this scenario, we will call the internal ALB directly through the peering connection.

Change the endpoint URL by executing the following command and replacing the value in braces with the output value on the console.

export URL={order_service_invoke_url}

After that, let’s run the ast_unauth_req.py and ast_auth_req.py scripts. They should return an HTTP status of 401 and 200 respectively.

python3 ast_unauth_req.py
python3 ast_auth_req.py

To destroy all the infrastructure provisioned in this section, navigate to the terraform/scenarios/scenario-2 directory and run the clean.sh file.

Make sure to destroy all the infrastructure related to the base model, navigate to the alb/terraform/scenarios/base-model directory and run the clean.sh file.

Wrap things up

I hope this guide has been helpful in understanding how to implement m2m authentication for services behind AWS ALB.

Have you implemented m2m authentication in your AWS environment? What challenges have you faced, and how have you overcome them?

--

--