Lambda function HTTP authorization with Auth0 and AssemblyLift (WebAssembly + Lambda + API Gateway + Rust)
Tags: serverless, aws, rust, cloud
Published: 2021-10-09 13:34:06

Oh, hello! Sorry, I didn't see you there.

Since you're here, let's talk about [Lambda](https://aws.amazon.com/lambda/) function authorization!

By itself, the only way a Lambda function has to reject an invocation is with an IAM policy. This will let you prevent another service from *programmatically* invoking the function via the AWS SDK. In most cases however it's considered bad practice to directly invoke a function in this way, due to the coupling it can introduce between two services.

A better approach is to invoke the function via HTTP [API Gateway](https://aws.amazon.com/api-gateway/), and make the underlying function an implementation detail. In addition to decoupling, with an API Gateway we can now use HTTP authorization schemes such as OAuth to protect our functions!

[Auth0](https://auth0.com/) offers a highly-regarded (and easy to use, personally speaking) authentication platform, which we can use to provide an OAuth authorizer for our API. Auth0 offers a free plan supporting up to 7000 users!

In this guide we'll use [AssemblyLift](https://assemblylift.akkoro.io) to deploy our Lambda function, and define our API and an authorizer. AssemblyLift is an open platform which allows you to quickly develop high-performance serverless applications. Service functions are written in the Rust programming language and compiled to WebAssembly. The AssemblyLift CLI takes care of compiling and deploying your code, as well as building and deploying all the needed infrastructure!

## Preparation & assumed knowledge
To follow this guide you will need an [AWS account](https://portal.aws.amazon.com/billing/signup#/start) if you do not have one already.

You will also need the [Rust toolchain](https://rustup.rs/) and [NPM](https://nodejs.org) installed. The Rust toolchain is installed using the `rustup` interactive installer. The default options during installation should be fine. After installation you will need to install the `wasm32-unknown-unknown` build target for the Rust toolchain (`rustup toolchain install wasm32-unknown-unknown`).

Once you have these prerequisites you can install AssemblyLift with `cargo install assemblylift-cli`. Run `asml help` to verify the installation.

⚠️**NOTE**⚠️: if you have previously installed AssemblyLift, please make sure you are on the latest version before you begin! Version `0.3.2` contains bugfixes affecting this guide. 🙂

### Assumed knowledge
We are going to write a simple function in [Rust](https://rust-lang.org) which writes an item to a [DynamoDB](https://aws.amazon.com/dynamodb/) table, so you should be familiar with Rust (our usage of DynamoDB will be very basic). In particular, you should be comfortable with [async/await in Rust](https://dev.to/dotxlem/async-rust-but-less-intimidating-2c13).

While this guide should be fairly straightforward, we will be using some *slightly* more advanced features of AssemblyLift. If this is your first time using AssemblyLift, consider taking a look [this post on deploying an Eleventy static site with AssemblyLift](https://dev.to/akkoro/deploy-an-ultra-fast-blog-in-minutes-with-eleventy-and-assemblylift-webassembly-lambda-api-gateway-rust-568l) which is a little simpler.

You should also be familiar with AWS IAM, as we will need to add permissions to our function enabling access to DynamoDB. Optionally if you know some [Terraform](https://terraform.io), we will also demonstrate how you can set these permissions from within the project code. 😀

## Project setup
In your favourite *projects* directory, create a new AssemblyLift application and change to that directory.

```
$ asml init -n auth-tutorial
$ cd auth-tutorial
```

Let's start by adding a `data` service, in which we will have a `put` function that allows us to add a new item to a DynamoDB table.

Generate the new service & function:
```
$ asml make service data
$ asml make function data.put
```

In order for AssemblyLift to recognize the new service, it must be added to the project manifest `assemblylift.toml`:
```toml
[project]
name = "auth0-tutorial"

[services]
data = { name = "data" }
```

If you like, you can delete the `default` service from the manifest as well as remove its directory under `services/`.

### Create an Auth0 API
With the base of our AssemblyLift project set up, now would be a good time to create a new API in Auth0. This will provide JWT authorization to our `data.put` function.

In your Auth0 dashboard, navigate to *Applications > APIs* and hit *Create API*.
![A screenshot of the "Create API" dialog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ltiqrtqjaihwiavrhswo.png)

Leave the signing algorithm as RS256, and give your API a name. The *identifier* is a unique string which is used within OAuth 2.0 to identify the authorization server which issued a token. It is recommended to use the URL of the endpoint which will be associated with this authorizer, however you are free to use whichever naming scheme you like!

## Configure the `data` service
Next let's open up the data service manifest `services/data/service.toml`, and replace the default configuration with the following:
```toml
# data/service.toml
[service]
name = "data"

[api.functions.put]
name = "put"
http = { verb = "PUT", path = "/put" }
authorizer_id = "auth0"

[api.authorizers.auth0]
auth_type = "JWT"
```

### Configure the authorizer
A `JWT` authorizer at minimum requires `audience` and `issuer` parameters. The *audience* is the *identifer* you chose for your API in the Auth0 console. The *issuer* is going to be a URL of the form `https://<tenant-id>.<region>.auth0.com`. You can find your tenant ID and region on the *settings* tab of the Auth0 console.

Add the parameters to the `auth0` definition:
```toml
# data/service.toml
[api.authorizers.auth0]
auth_type = "JWT"
audience = ["your-identifier-here"]
issuer = "https://<tenant-id>.<region>.auth0.com"
```

## Writing data with the `put` function
This will be a relatively simple function which writes some data to a DynamoDB table, as an example of an operation that really ought to be protected by an authorizer 😜

We will need to add a dependency to the DynamoDB [IOmod](https://docs.assemblylift.akkoro.io/learn-assemblylift/io-modules/registry). IOmod dependencies are defined in `service.toml`:
```toml
# data/service.toml
[iomod.dependencies.dynamodb]
coordinates = "akkoro.aws.dynamodb"
version = "0.1.5"
```

We will also need to add a Cargo dependency to our `put` function. IOmod dependencies are paired with "guest crates" which provide an API to the IOmod. If you are familiar with the C or C++ programming language, you can think of this as analogous to a header file.
```toml
# Cargo.toml
[dependencies]
assemblylift-iomod-dynamodb-guest = "0.1"
```

Next let's look at the function code. Find `put/src/lib.rs` and update it with the following:
```rust
// data/put/src/lib.rs
extern crate asml_awslambda;

use asml_core::GuestCore;
use asml_awslambda::*;

use assemblylift_iomod_dynamodb_guest::{put_item, structs::*};

handler!(context: LambdaContext<ApiGatewayEvent>, async {
    let user_id = context.event.request_context
                .unwrap()
                .authorizer
                .unwrap()
                .claims
                .unwrap()
                .get("sub")
                .unwrap()
                .to_string();

    let mut input = PutItemInput::default();
    input.table_name = "auth0-tutorial".to_string(); // Change this to the name of your own DynamoDB table!
    input.item = PutItemInputAttributeMap::default();

    let mut pk_value = AttributeValue::default();
    pk_value.s = Some(user_id);
    input.item.insert(AttributeName::from("pk"), pk_value);

    match put_item(input).await {
        Ok(response) => http_ok!(response),
        Err(err) => http_error!(err.to_string()),
    }
});
```

This function calls the DynamoDB [`PutItem`](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) action, to write the authenticated user's ID to a table called `auth0-tutorial` with a primary key named `pk`. The user ID is written as the primary key, as you might if this were a real table tracking user data.

All IOmod calls in Rust return a type implementing `Future`, giving us support for `await` syntax! The action is called with the `input` struct we built, and an HTTP response is returned when the action is resolved.

### Function permissions
In order for our function to access DynamoDB, we will have to attach an IAM policy to the function's execution role which allows access.

Unfortunately this is not handled in the AssemblyLift TOML directly at the moment (we are taking our time sorting out how permissions will be modelled in AssemblyLift 🙂). Your options are to find the role in the [AWS IAM console](https://console.aws.amazon.com/iamv2/home?#/roles) and add the policy there, or you can use a feature of AssemblyLift which lets you include your own Terraform code with the code generated by the CLI!

AssemblyLift generates IAM roles for functions named in a particular format; `asml-{project name}-{service name}-{function name}`.

Your own Terraform can be included with the AssemblyLift project by adding a Terraform module inside a directory named `user_tf` at the project root (next to `assemblylift.toml`).

An example of such a module is below:
```terraform
provider aws {
  region = "us-east-1"
}

data aws_caller_identity current {}
data aws_region current {}

locals {
    account_id = data.aws_caller_identity.current.account_id
    region     = data.aws_region.current.name

    prefix     = "asml-auth0-tutorial"
    table      = "auth0-tutorial"
}

data aws_iam_role data-put {
    name = "${local.prefix}-data-put"
}

data aws_iam_policy_document allow-dynamodb-put {
    statement {
      actions   = ["dynamodb:PutItem"]
      effect    = "Allow"
      resources = ["arn:aws:dynamodb:${local.region}:${local.account_id}:table/${local.table}"]
    }
}

resource aws_iam_policy data-put-policy {
    name   = "${data.aws_iam_role.data-put.name}-dynamodb"
    policy = data.aws_iam_policy_document.allow-dynamodb-put.json
}

resource aws_iam_role_policy_attachment data-put-policy {
    role       = data.aws_iam_role.data-put.name
    policy_arn = aws_iam_policy.data-put-policy.arn
}
```

Terraform's `data` lookup feature is used to import the role AssemblyLift created for you. A caveat here is that you must have run `asml bind` at least once prior to adding this lookup, in order for it to be found! See the next section below on building & deploying!

## Build & deploy the `data` service!
This part is quite easy with AssemblyLift! ☺️

Use the `cast` command to compile your function code, and generate a Terraform plan. Then use the `bind` command to deploy the serialized code & infra to AWS!

Before running `bind`, check the output of all the processes executed during `cast` to verify that the function compiled OK and that the infrastructure changes reported by Terraform aren't unexpected.

```bash
$ asml cast
$ asml bind
```

## Testing our function authorizer
The easiest way to test the authorizer is to use the `curl` commands generated for you in the Auth0 dashboard! Navigate to the *test* tab of your API in the dashboard. The command you are looking for is of the form:
```bash
curl --request GET \
  --url http://path_to_your_api/ \
  --header 'authorization: Bearer <TOKEN>'
```

Your API endpoint URL can be found in the [AWS API Gateway console](https://console.aws.amazon.com/apigateway/main/apis). This endpoint corresponds to the `data` service. Our `put` function is at the path (and verb) we defined in the service definition TOML:
```bash
curl --request PUT \
  --url https://<api-id>.execute-api.<region>.amazonaws.com/put \
  --header 'authorization: Bearer <TOKEN>'
```

Calling the endpoint **without** the authorization header should return an HTTP Forbidden error. Otherwise it should return HTTP OK with an empty JSON object `{}`!

Open the [DynamoDB web console](https://console.aws.amazon.com/dynamodbv2/home) and verify that the PutItem call worked; there should be an item with your Auth0 user ID!

## That's all, folks!
If you have any questions, please don't hesitate to reach out in the comments below or [on GitHub](https://github.com/akkoro/assemblylift/discussions)!