Using .NET AWS Lambda Authorizer To Secure API Gateway REST API

Lambda Authorizer is a feature provided by API Gateway that helps us separate the authentication logic from our business logic in our function code. Let's learn how to build a Lambda Authorizer in .NET Core and use it to secure an API Gateway REST API.

Rahul Pulikkot Nath
Rahul Pulikkot Nath

Table of Contents

This article is sponsored by AWS and is part of my AWS Series.

When building serverless APIs with AWS Lambda and API Gateway, one of the most critical questions is how to secure the API.

Lambda Authorizers are a feature provided by API Gateway that helps us separate the authentication logic from our business logic in our function code.

In this blog post, let's explore all about Lambda Authorizers in Amazon API Gateway using .NET Core.

You will learn:

  • Build an AWS Lambda Authorizer using .NET Core
  • Set up Lambda Authorizer in API Gateway
  • Caching Authorizer Responses in API Gateway
  • Pass data from Authorizer to Lambda Function code

If you are new to building REST API using .NET and Amazon API Gateway, check out the below article to get started.

How To Build an API Gateway REST API Using AWS Lambda Proxy Integration?
In this post, you will learn how to build a REST API using Amazon API Gateway with AWS Lambda Proxy integration built in .NET Core. Learn how to build and set up the Lambda integration, connect to a DynamoDB database and perform CRUD operations.

For this post, I will use the API Gateway REST API built in the above article.

.NET Core Lambda Function

To set up an Authorizer for API Gateway, we first need to build a Lambda Function. With the AWS Toolkit installed for Visual Studio, use the Lambda Function and the Empty template to build a Lambda Authorizer function. Install the Amazon.Lambda.APIGatewayEvents NuGet package to get the API Gateway custom authorizer request/response classes - APIGatewayCustomAuthorizerRequest and APIGatewayCustomAuthorizerResponse.

The Type and MethodArn property on the APIGatewayCustomAuthorizerRequest object is populated for all request types. Type indicates the type of Authorizer, and the MethodArn indicates the method for which the Lambda Authorizer was invoked.

Types of Lambda Authorizer

There are two types of Lambda Authorizers.

  • TOKEN Authorizer → Receives the caller’s identity in a bearer token, e.g., JWT or OAuth token
  • REQUEST Authorizer → Receives the caller’s identity in a combination of headers, query string parameters, stage, and context variables.

The type of Authorizer is decided based on how it’s set up in the API Gateway when we add the Custom Authorizer. (We will see this later in the post).

Based on the type of the Authorizer, the request parameters that come into the Lambda Authorizer Function are different. When using the Token Authorizer, the AuthorizationToken property is populated with the bearer token from the incoming request. API Gateway does this automatically. When using Request Authorizer, the AuthorizationToken property is null, and all other properties, Headers, QueryStringParameters, PathParameters, StageVariables etc., are populated.

Sample Token Validation using .NET

The validation mechanisms change based on the type of token and how it’s generated. If you have an Identity server setup for your organization, use that to validate tokens and retrieve associated details. This might involve an additional HTTP call to the Identity Server.

For this blog post, I am using JSON Web Token Builder to generate test tokens. To validate the token, I use the JwtSecurityTokenHandler class and the privateKey used to sign the token (in that online tool).

private static ClaimsPrincipal? ValidateToken(string authToken)
    {
        const string privateKey = "qwertyuiopasdfghjklzxcvbnm123456";
        var tokenHandler = new JwtSecurityTokenHandler();
        var validationParameters = new TokenValidationParameters()
        {
            ValidateLifetime = false,
            ValidateAudience = false,
            ValidateIssuer = false,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(privateKey)) 
        };

        try
        {
            return tokenHandler.ValidateToken(authToken, validationParameters, out SecurityToken validatedToken);
        }
        catch (Exception)
        {
            return null;
        }
    }

If the token is valid, it returns a ClaimsPrincipal object instance which contains information about the token.

Below is the decoded payload of the test JWT token I am using.

{
  "iss": "Online JWT Builder",
  "iat": 1658808785,
  "exp": 1690344785,
  "aud": "www.example.com",
  "sub": "hello@rahulpnath.com",
  "GivenName": "Rahul",
  "Surname": "Nath",
  "Email": "hello@rahulpnath.com",
  "Role": [
    "Admin",
    "User"
  ],
  "UserId": "e660c2dd-2688-44de-82d1-41590ae4aace"
}

Use the appropriate key names to retrieve the claims from the ClaimsPrincipal. Below is an example of retrieving the userId value from the Claims.

var userId = claimsPricipal.FindFirst("UserId")?.Value;

API Gateway Authorized and Unauthorized Responses

Lambda Authorizer returns the same response object, APIGatewayCustomAuthorizerResponse, for both authorized and unauthorized responses.

return new APIGatewayCustomAuthorizerResponse()
{
    PrincipalID = userId,
    PolicyDocument = new APIGatewayCustomAuthorizerPolicy()
    {
        Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement>
        {
            new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement()
            {
                Effect = "Allow",
                Resource = new HashSet<string> { request.MethodArn },
                Action = new HashSet<string> { "execute-api:Invoke"}
            }
        }
    }
};

If authorized, it specifies Resource, a list of ARNs it provides access for, and also the list of Action allowed. In our example, since the authorizer is for accessing an API endpoint, we return the MethodArn and provide the appropriate permissions. In this case, execute-api:Invoke permission to invoke the Lambda function.

Setting Up Authorizer in API Gateway

Now that we have the Authorizer Lambda function up and running in our AWS account let’s set it up as an Authorizer in API Gateway.

Under the Authorizers section for the REST API in Amazon API Gateway, select ‘Create New Authorizer’. This shows the below dialog to enter the Lambda Function details, the Lambda Event Payload (Token Type), and other information for the Authorizer.

Below I create a Token based authorizer, user-service-authorizer, which uses the HTTP header authorizationToken to get the Bearer Token.

Create a new .NET Lambda Authorizer in API Gateway REST API by providing the name, Lambda function used as the authorizer, and the token type to use. Also, enable token caching if required.

You can create multiple Authorizers if required for the same REST API. Creating an Authorizer here does not apply it to the API automatically.

We need to set the Authorizer explicitly for each Method endpoint for the API. This is set under the Method Request section under a Resoruce.

E.g., Below for the GET method on the Users resource, set the Authorization to the new user-service-authorizer

Set the .NET Lambda Authorizer on REST API Method Request

For each method on the REST API, the Authorizer needs to be selected explicitly.

You can add Header and Query parameter validations if the Authorizer expects specific values to be present in the HTTP request.

Deploy & Test

The Lambda Authorizer can be tested only after deploying to a Stage. Using the Test client within the Resource section of the API Gateway does not invoke the Lambda Authorizers. It only invokes the Lambda function set up in the Integration Request section of the Method.

To test our new Custom Lambda Authorizer, deploy the API to a Stage.

Once deployed, make a GET or POST request to the API endpoint from Postman. Pass the token in the authorizationToken HTTP header value.

Sample API gateway REST API request with the token in HTTP Header for .NET Lambda Authorizer.

Caching API Gateway Authorizer Responses

Every time we make a call to the Resource endpoint, it now has to make two round-trip calls.

  • One to the Lambda Authorizer function, to check whether the caller is authorized or not
  • One to the actual Lambda Function if the caller is authorized.

This impacts the overall end-to-end response time on the API Gateway endpoint.

With API Lambda Authorizer, you can cache the response at the API Gateway based on a key. The key is based on the Authorizer type selected.

  • Token Type → The token value is used as the key
  • Request Type → All the keys selected

The response from the Authorizer lambda is cached at the API Gateway for the configured time. During that time, if another request comes with the same key, API Gateway uses the cached response from the previous request.

Caching and Policies

The Authorizer cache is at the API Gateway level. So if both GET and POST requests use an Authorizer, the response should enable all the methods the token has access to. This ensures that if the same user makes subsequent calls to different Methods (using the same Authorizer), the API Gateway will allow the method to be accessed. Otherwise, the cached token will have access only to the first method that triggered a call to the authorizer until the token is removed from the cache.

Updating our initial code, instead of just specifying the calling method ARN back with the policies, we need to ensure we return all the methods the token/user has access to.

For example, below, I have updated the Resource property of the returned IAMPolicyStatement class to specify “*”, to indicate it has access to all methods. This is possible only in scenarios where the user is in an Admin role and has access to all functionality.

return new APIGatewayCustomAuthorizerResponse()
{
    ...,
    PolicyDocument = new APIGatewayCustomAuthorizerPolicy()
    {
        Statement = new List<APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement>
        {
            new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement()
            {
                Effect = "Allow",
                Resource = new HashSet<string> { "*" }, // Or explicitly specify methods.
                Action = new HashSet<string> { "execute-api:Invoke"}
            }
        }
    }
};

For other users, you can explicitly return the method ARNs that the user can access based on their role. The application can use conventions or will need to keep a map of roles vs. methods to return this information.

Pass Custom Data From Authorizer to Lambda Function Code

Often we need information about the User accessing the function to make business decisions. We might also need this to save user details as part of the data stored or for logging/auditing.

Since the token-related information is available in the Lambda Authorizer, we need a way to pass this information to the Lambda function processing the request.

The request context can be used to pass information from the Lambda Authorizer to the Lambda function code.

var userId = claims.FindFirst("UserId")?.Value;
var email = claims.FindFirst("Email")?.Value;
var givenName = claims.FindFirst("GivenName")?.Value;
var surname = claims.FindFirst("Surname")?.Value;
var roles = string.Join(",", claims.FindAll("Role").Select(x => x.Value));

return new APIGatewayCustomAuthorizerResponse()
{
    ...,
    Context = new APIGatewayCustomAuthorizerContextOutput()
    {
        {"UserId", userId },
        {"Email", email },
        {"Name", $"{givenName} {surname}" },
        {"Roles", roles }
    }
};

The content passed via the Context property of the Lambda Authorizer response is available in the APIGatewayProxyRequest under the RequestContext.Authorizer property.

public async Task<APIGatewayProxyResponse> FunctionHandler(
    APIGatewayProxyRequest request, ILambdaContext context)
{
    var userId = request.RequestContext.Authorizer["UserId"];
    ...
}

These values can be used for business logic, logging, etc, as required by your application code.

Full Source code and demo available here.

I hope this helps you start using Lambda Authorizer for authenticating requests coming to the API endpoint.

Photo by Ray Hennessy on Unsplash

AWSServerlessDotnet-Core