Writing about Cloud, architecture, AWS, GCP and software engineering.

Golang JWT authorizer for AWS API Gateway

July 5, 2023
Source code: GitHub

When using AWS API Gateway you can use the AWS Lambda authorizer for HTTP APIs to authorize the requests. In this blog I will show you how to validate a JWT token signed with KMS in a Lambda using the Golang runtime. For the examples I am using API Gateway V2 with HTTP APIs with the v2 authorizer payload format version and for the resources I am using Terraform.

Why would you want to validate the JWT at the Gateway?

There are multiple benefits of validating the JWT token at the Gateway:

  • Not having to implement JWT logic in every service, reducing maintenance overhead
  • Increasing performance by caching the authorizer result for subsequent requests
  • Increasing security by slowing down attackers by using rate limiting and caching the authorizer result

If you decided not to implement JWT validation logic in every service you are relying fully on the API Gateway which could be a security risk. By having every service validate the JWT token you decrease the blast radius and making sure the API Gateway is not a single point of failure. So you have to look closely to your use case and business requirements and make a decision based on that.

How to set up the Lambda Authorizer

Below you find Golang Lambda code to validate the JWT token, the code can also be found on GitHub.

package main

import (
	"context"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/kms"
	"github.com/matelang/jwt-go-aws-kms/v2/jwtkms"
	"github.com/golang-jwt/jwt/v5"
	"github.com/sirupsen/logrus"
)

var log = logrus.New()

var KmsKeyID = os.Getenv("KMS_JWT_KEY_ID")

func HandleRequest(ctx context.Context, request events.APIGatewayCustomAuthorizerRequestTypeRequest) (events.APIGatewayV2CustomAuthorizerSimpleResponse, error) {
	awsConfig, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		log.Errorf("aws config failed to load %s", err)

		return events.APIGatewayV2CustomAuthorizerSimpleResponse{
			IsAuthorized: false,
		}, nil
	}

	kmsConfig := jwtkms.NewKMSConfig(kms.NewFromConfig(awsConfig), KmsKeyID, false)

	claims := jwt.RegisteredClaims{}

	_, err = jwt.ParseWithClaims(request.Headers["authorization"],
		&claims, func(token *jwt.Token) (interface{}, error) {
			return kmsConfig, nil
		})
	if err != nil {
		log.Errorf("can not parse/verify token %s", err)

		return events.APIGatewayV2CustomAuthorizerSimpleResponse{
			IsAuthorized: false,
		}, nil
	}

	log.Infof("validated token with claims: %v", claims)

	return events.APIGatewayV2CustomAuthorizerSimpleResponse{
		IsAuthorized: true,
	}, nil
}

func main() {
	lambda.Start(HandleRequest)
}

To add the Lambda Authorizer to the API Gateway you’ll need the following resources:

resource "aws_apigatewayv2_authorizer" "jwt" {
  api_id                            = APIGATEWAY_ARN // Add your API Gateway here
  authorizer_type                   = "REQUEST"
  authorizer_uri                    = aws_lambda_function.jwt_authorizer.invoke_arn
  identity_sources                  = ["$request.header.Authorization"]
  name                              = "jwt-authorizer"
  authorizer_payload_format_version = "2.0"
  enable_simple_responses           = true
}

resource "aws_lambda_function" "jwt_authorizer" {
  s3_bucket = "BUCKET_NAME"
  s3_key = "KEY"

  function_name = "jwt-authorizer"
  role          = aws_iam_role.lambda_execution.arn // The role needs 'kms:GetPublicKey' permissions
  handler       = "jwt-validator-lambda"

  environment {
    variables = {
      KMS_JWT_KEY_ID = aws_kms_key.jwt.id
    }
  }

  runtime = "go1.x"
}

resource "aws_lambda_permission" "api_gateway" {
  statement_id  = "AllowExecutionFromApiGateway"
  action        = "lambda:InvokeFunction"
  function_name = "jwt-authorizer"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "arn:aws:execute-api:REGION_NAME:ACCOUNT_ID:${aws_apigatewayv2_api.main.id}/authorizers/${aws_apigatewayv2_authorizer.jwt.id}"
}

resource "aws_kms_key" "jwt" {
  description             = "jwt"
  deletion_window_in_days = 7

  customer_master_key_spec = "ECC_NIST_P521"
  key_usage                = "SIGN_VERIFY"
}

The Lambda function needs kms:GetPublicKey permissions to read the public key used to sign the JWT token.

Conclusion

In conclusion, validating JWT tokens at the API Gateway offers benefits such as reduced maintenance overhead, improved performance, and increased security. However, it is important to weigh these advantages against the potential risks and consider the specific needs of your application before deciding on the best approach.