# How to Use Lambda Authorizers to Validate Microsoft EntraID (Azure AD) Tokens in Amazon API Gateway

Sometimes, we need custom or flexible authorization logic that goes beyond the built-in capabilities of Amazon API Gateway, such as [AWS IAM Authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html), [Amazon Cognito User Pools Authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html), or [API Gateway Resource Policies](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-resource-policies.html). For these scenarios, [Lambda Authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) can be a great fit. There are two types:

* **Request-based**: The authorizer receives different parts of the request, such as headers, query string parameters, and body. That allows for building complex authorization logic.
    
* **Token-based**: The authorizer receives only a token, usually JWTs or OAuth tokens. Typically performs better than the request-based type due to a reduced input size and simple caching logic.
    

Regardless of the type, the authorization process follows these steps:

* A client sends a request to the Amazon API Gateway
    
* The Lambda authorizer is invoked and performs the validation logic.
    
* The Lambda authorizer generates an IAM policy granting or denying access.
    
* The API Gateway determines whether the request is allowed or denied based on the policy.
    

With this understanding, let's build our own Lambda authorizer to validate a token generated by Microsoft Entra ID and secure our Amazon API Gateway endpoints.

## Pre-requisites

* An [IAM User](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console) with programmatic access.
    
* Install the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-creds).
    
* Install the Amazon Lambda Templates (`dotnet new -i Amazon.Lambda.Templates`)
    
* Install the Amazon Lambda Tools (`dotnet tool install -g Amazon.Lambda.Tools`)
    
* Install [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html).
    
* A registered application in [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app).
    

## The Lambda function

First, let's build the Lambda function behind the Amazon API Gateway. Run the following commands to set up our Lambda function:

```powershell
dotnet new lambda.EmptyFunction -n MyLambda -o .
dotnet add src/MyLambda package Amazon.Lambda.APIGatewayEvents
dotnet new sln -n MyApplications
dotnet sln add --in-root src/MyLambda
```

Open the `Program.cs` file and update the content as follows:

```csharp
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Text.Json;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace MyLambda;

public class Function
{
    public APIGatewayHttpApiV2ProxyResponse FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        context.Logger.LogInformation(JsonSerializer.Serialize(input));
        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = @"{""Message"":""Hello World""}",
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }
}
```

## The Authorizer Lambda function

Run the following commands:

```powershell
dotnet new lambda.EmptyFunction -n MyAuthorizerLambda -o .
dotnet add src/MyAuthorizerLambda package Amazon.Lambda.APIGatewayEvents
dotnet add src/MyAuthorizerLambda package Microsoft.IdentityModel.Protocols.OpenIdConnect
dotnet sln add --in-root src/MyAuthorizerLambda
```

Open the `Program.cs` file and update the content as follows:

```csharp
using Amazon.Lambda.Core;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Amazon.Lambda.APIGatewayEvents;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace MyAuthorizerLambda;

public class Function
{
    private string _tenantId;
    private string _clientId;
    private ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;

    public Function()
    {
        _tenantId = Environment.GetEnvironmentVariable("TENANT_ID")!;
        _clientId = Environment.GetEnvironmentVariable("CLIENT_ID")!;
        string metadataEndpoint = $"https://login.microsoftonline.com/{_tenantId}/v2.0/.well-known/openid-configuration";
        _configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());
    }

    public async Task<APIGatewayCustomAuthorizerResponse> FunctionHandler(APIGatewayCustomAuthorizerRequest request, ILambdaContext context)
    {
        var authorized = false;
        var principalId = "user";
        try
        {
            var openIdConnectConfiguration = await _configurationManager.GetConfigurationAsync();
            var validationParameters = new TokenValidationParameters
            {
                ValidateAudience = true,
                ValidateIssuer = true,
                ValidateIssuerSigningKey = true,
                ValidateLifetime = true,
                ValidAudience = $"api://{_clientId}",
                IssuerSigningKeys = openIdConnectConfiguration.SigningKeys,
                ValidIssuer = $"https://sts.windows.net/{_tenantId}/"
            };
            var tokenHandler = new JwtSecurityTokenHandler();
            var principal = tokenHandler.ValidateToken(request.AuthorizationToken, validationParameters, out var securityToken);
            authorized = true;
            principalId = principal.Identity?.Name;
        }
        catch (Exception ex)
        {
            context.Logger.LogError($"Error occurred validating token: {ex.Message}");
            authorized = false;
        }
        var policy = new APIGatewayCustomAuthorizerPolicy
        {
            Version = "2012-10-17",
            Statement =
            [
                new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement
                {
                    Action = new HashSet<string>(new string[] { "execute-api:Invoke" }),
                    Effect = authorized ? "Allow" : "Deny",
                    Resource = new HashSet<string>(new string[] { request.MethodArn })
                }
            ]
        };
        var contextOutput = new APIGatewayCustomAuthorizerContextOutput
        {
            ["Path"] = request.MethodArn
        };
        return new APIGatewayCustomAuthorizerResponse
        {
            PrincipalID = principalId,
            Context = contextOutput,
            PolicyDocument = policy
        };
    }
}
```

A brief description of what we are doing here:

* The `APIGatewayCustomAuthorizerRequest` class receives the [request](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html) from the AWS API Gateway.
    
* The signing keys are retrieved using the information provided by the .well-known endpoint.
    
* The `TokenValidationParameters` class specifies what we need to validate from the token.
    
* The `JwtSecurityTokenHandler` class validates and reads the token.
    
* The `APIGatewayCustomAuthorizerResponse` is used as a [response](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html) and includes:
    
    * `APIGatewayCustomAuthorizerPolicy`: Represents the IAM policy denying or allowing access to a resource. Defining the right resource determines how generic and reusable the Lambda authorizer can be.
        
    * `APIGatewayCustomAuthorizerContextOutput`: Amazon API Gateway sends this object to the backend Lambda function as part of the input.
        
    * `PrincipalID`: The user identification associated with the token.
        

## AWS SAM template

Create a `template.yml` file with the following content:

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  SAM

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Auth:
        Authorizers:
          MyAuthorizerLambda:
            FunctionArn: !GetAtt MyAuthorizerFunction.Arn

  MyApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet8
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        Get:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /hello-world
            Method: get
            Auth:
              Authorizer: MyAuthorizerLambda

  MyAuthorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      Environment:
        Variables:
          TENANT_ID: "<MY_TENANT_ID>"
          CLIENT_ID: "<MY_CLIENT_ID>"
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet8
      Architectures:
        - x86_64    
      Handler: MyAuthorizerLambda::MyAuthorizerLambda.Function::FunctionHandler
      CodeUri: ./src/MyAuthorizerLambda/

Outputs:
  MyApiEndpoint:
    Description: "API endpoint"
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello-world"
```

The resources defined in the file are:

* The Lambda authorizer corresponds to the resource named `MyAuthorizerFunction`. The `ApplicationID/ClientID` and the `DirectoryID/TenantID` come from the App Registration in Microsoft Entra ID.
    
* Inside the `AWS::Serverless::Api` resource, the `Auth` [property](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-api-apiauth.html) defines a list of `Authorizers`. In this case, the `MyAuthorizerLambda` authorizer points to the previous resource.
    
* Finally, the Lambda function behind the Amazon API Gateway is the MyApiFunction resource, which uses the `MyAuthorizerLambda` authorizer defined at `AWS::Serverless::Api` level.
    

Run the following commands to deploy the resources to AWS:

```powershell
sam build
sam deploy --guided
```

## Testing

In this case, we are using Postman to generate the token and include it in our request to the endpoint:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1715896423155/0c6af437-9865-4a4b-8bff-f7867f58c956.png align="center")

You can find the code and scripts [here](https://github.com/raulnq/aws-api-gateway-lambda-authorizer). Thank you, and happy coding.
