Controlling access to the API with Lambda authorizers
Tags: aws, security, cloud, serverless
Published: 2022-05-05 18:36:08

Custom Lambda authorizers can also help protect our APIs. We can set up a token-based authorizer that validates the existence of a specific header. If the token is present in the request, the authorizer will allow API Gateway to process the request. Otherwise, it will deny it.

## 1. The scenario

Alice, who wanted to [protect her company's API using CloudFront](https://dev.to/aws-builders/signing-requests-to-aws-services-using-axios-1043), decided to look for a slightly different solution to the same problem.

To recap, she has an API created in <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">Amazon API Gateway</a>.

Her goal is that API Gateway should deny every request that doesn't contain a specific header. Alice decided to call it `x-secret-header`, but it could have any other name. If the request includes this header and the header has the correct value, API Gateway will allow the request to proceed to the integration.

Alice doesn't want to use the well-known `x-api-key` header, and she prefers a key instead that only her team and the application know.

For this case, she can build a **lambda authorizer**, which checks for the existence of the required header.

## 2. About Lambda authorizers

**Lambda authorizers** are <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> functions that integrate with API Gateway. They are great for validating custom authorization schemes.

Lambda authorizers are one of the multiple authorization methods API Gateway supports. One of their advantages is that they are very flexible and allow fine-grained permission management.

They can be `TOKEN` or `REQUEST` based. A `REQUEST`-based Lambda authorizer uses the combination of headers, query parameters, stage variables, and context variables to decide if the requester can invoke the API. For `TOKEN`-based authorizer functions, a specific token header must be present.

Alice's solution uses a `TOKEN`-based Lambda authorizer.

## 3. Architecture

The diagram below shows how the `TOKEN`-based Lambda authorizer in this solution works.

![Simple Lambda authorizer architecture example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubh4i1421nk7fml6itre.png)

When a request comes into API Gateway, the service will invoke the Lambda authorizer. The authorizer function code investigates if the value of the header is correct.

This architecture has the secret header value stored in <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" class="link" target="_blank" rel="noopener noreferrer">Parameter Store</a>. When API Gateway invokes the authorizer function, it will fetch the secret from Parameter Store and will compare it to the incoming token.

Depending on the outcome of the evaluation process, the authorizer function must return a **policy** document with either an `ALLOW` or `DENY` effect.

If API Gateway receives a policy back from the authorizer with an `Effect: Deny`, it will respond with a `401 Unauthorized` status code.

If the assessment result is `Effect: Allow`, API Gateway will let the request go through.

## 4. Lambda authorizer code

Let's see the code for a `TOKEN`-based Lambda authorizer function in <a href="https://www.typescriptlang.org/" class="link" target="_blank" rel="noopener noreferrer">TypeScript</a>. Ideally, parts of this code should be in different modules, but I have them in just one function (`index.ts`) for brevity.

```ts
import { GetParameterCommandInput, SSM } from '@aws-sdk/client-ssm'
import { APIGatewayTokenAuthorizerEvent } from 'aws-lambda'

enum Effect {
  ALLOW = 'allow',
  DENY = 'deny',
}

const ssm = new SSM({
  region: 'us-east-1',
})

export async function handler(event: APIGatewayTokenAuthorizerEvent) {
  const { methodArn: resource, authorizationToken: secretHeader } = event

  if (!secretHeader) {
    return generatePolicy(resource, Effect.DENY)
  }

  const params: GetParameterCommandInput = {
    Name: 'x-secret-header',
    WithDecryption: true,
  }

  const secretParameter = await ssm.getParameter(params)
  const secretValue = secretParameter.Parameter?.Value

  if (!secretValue) {
    return generatePolicy(resource, Effect.DENY)
  }

  if (secretHeader === secretValue) {
    return generatePolicy(resource, Effect.ALLOW)
  }

  return generatePolicy(resource, Effect.DENY)
}

function generatePolicy(resource: string, effect: Effect) {
  return {
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Resource: resource,
        Effect: effect
      }]
    },
    principalId: 'mySecretUser',
  }
}
```
### 4.1. Dependencies

I used the <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/" class="link" target="_blank" rel="noopener noreferrer">AWS SDK for JavaScript v3</a> for creating the client for accessing Parameter Store.

The advantage of using version 3 of the SDK is that the library is modular, and we don't have to install the whole SDK package for just one service. This way, our Lambda deployment package will be smaller.

### 4.2. A brief explanation of the code

The policy generator function (`generatePolicy`) creates a required authorizer component. It must return an object with at least `policyDocument` and `principalId` properties.

The ARN of the API method that the client invokes comes from the `methodArn` property of the `event`, while the authorization header is available from the `authorizationToken` property. We have to set up `x-secret-header` in API Gateway (see next point). Then the service will take the header value and transfer it to the `authorizationToken` property. The Lambda authorizer will get the token from this property which is part of the `event` argument.

The rest of the code is straightforward. The authorizer checks the existence of the header and gets the secret value from Parameter Store. If everything looks good, the function will return the policy with `Effect: Allow`.

The complete response object looks like this:

```jsonc
{
  "principalId": "yyyyyyyy", // The principal user identification associated with the token sent by the client.
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow|Deny",
        "Resource": "arn:aws:execute-api:{regionId}:{accountId}:{apiId}/{stage}/{httpVerb}/[{resource}/[{child-resources}]]"
      }
    ]
  },
  "context": {
    "stringKey": "value",
    "numberKey": "1",
    "booleanKey": "true"
  },
  "usageIdentifierKey": "{api-key}"
}
```

The `context` and `usageIdentifierKey` properties are optional. I'll have a link in the References section where you can find more information on them if you want to learn more.

## 5. Main steps in API Gateway

I won't write how to create the authorizer (there is a link in the References section). Instead, I will highlight the main steps.

![Token-based Lambda authorizer settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lewgzphdz9azqe291jfc.png)

### 5.1. Set TOKEN

We have to set `TOKEN` the **Lambda Event Payload** because the authorizer is `TOKEN`-based.

### 5.2. Specify the secret header

We specify the `x-secret-header` as the **Token Source**. API Gateway will automatically convert the header to the `authorizationToken` property, which the Lambda authorizer can access.

### 5.3. Set token validation

The token in this example consists of 32 random characters (small and capital letters and numbers).

The **Token Validation** setting accepts a regular expression. If we specify it, API Gateway will validate the token. API Gateway will only call the Lambda authorizer function if the validation is successful.

This feature is great because we can save money on Lambda invocations. If the header is not of the correct format (that is, it's not 32 characters long or contains special characters), API Gateway won't call the authorizer function. This way, we don't have to pay for unnecessary invocations.

It will only work if we write the regex without `//`.

### 5.4. Permission to call the authorizer

API Gateway needs a permission to call the authorizer. When we create the authorizer, we can specify an <a href="https://aws.amazon.com/iam/" class="link" target="_blank" rel="noopener noreferrer">IAM</a> role with the relevant permissions or leave the role field empty.

We don't have to specify the role. In this case, API Gateway will add the relevant permission statement to the authorizer function's resource-based policy.

## 6. Considerations

Lambda authorizers are for **fine-grained** authorization. It means that we can validate different types of tokens. This example above has a secret string but we can also validate <a href="https://jwt.io/introduction" class="link" target="_blank" rel="noopener noreferrer">JWTs</a> or use <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme" class="link" target="_blank" rel="noopener noreferrer">basic authentication</a>. In these cases the name of the header will be different, and we'll need to change the Token Source setting in API Gateway accordingly.

API Gateway can invoke the authorizer function more times than we think. It might result in unexpected **costs**. We can reduce the number of invocations (and save money) by enabling **Token Validation** given the token format allows it. We should also optimize the authorizer function's memory to become more cost-effective.

We can enable **Authorization Caching** too, which will help us further reduce the cost of Lambda. When caching is enabled, API Gateway will store the generated policy for the given user. It won't call the authorizer if the same user makes a request to the endpoint within the `TTL` period. In the example above, `x-secret-header` will be the cache key which will always have the same value. In this case, API Gateway will only call the authorizer function after the time specified in `TTL` has expired.

## 7. Summary

Lambda authorizers provide a way for fine-grained authorization schemes when we have to protect our API in API Gateway. We can customize the authorizer to validate different types of authorization formats.

The flexibility comes at the price of writing custom code, which means more work for developers. Cost can also be a factor, so we should carefully plan the type of authorization.

## 8. References and further reading

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" class="link" target="_blank" rel="noopener noreferrer">Use API Gateway Lambda authorizers</a> - Everything about Lambda authorizers: set up, input payload, output, testing