DEV Community

Cover image for Lambda function HTTP authorization with Auth0 and AssemblyLift (WebAssembly + Lambda + API Gateway + Rust)
Dan for Akkoro

Posted on

Lambda function HTTP authorization with Auth0 and AssemblyLift (WebAssembly + Lambda + API Gateway + Rust)

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

Since you're here, let's talk about 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, 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 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 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 if you do not have one already.

You will also need the Rust toolchain and NPM 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 which writes an item to a 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.

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 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, 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 for your blog and change to that directory.

$ asml init -n auth-tutorial
$ cd auth-tutorial
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In order for AssemblyLift to recognize the new service, it must be added to the project manifest assemblylift.toml:

[project]
name = "auth0-tutorial"

[services]
data = { name = "data" }
Enter fullscreen mode Exit fullscreen mode

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

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:

# 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"
Enter fullscreen mode Exit fullscreen mode

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:

# data/service.toml
[api.authorizers.auth0]
auth_type = "JWT"
audience = ["your-identifier-here"]
issuer = "https://<tenant-id>.<region>.auth0.com"
Enter fullscreen mode Exit fullscreen mode

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. IOmod dependencies are defined in service.toml:

# data/service.toml
[iomod.dependencies.dynamodb]
coordinates = "akkoro.aws.dynamodb"
version = "0.1.5"
Enter fullscreen mode Exit fullscreen mode

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.

# Cargo.toml
[dependencies]
assemblylift-iomod-dynamodb-guest = "0.1"
Enter fullscreen mode Exit fullscreen mode

Next let's look at the function code. Find put/src/lib.rs and update it with the following:

// 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()),
    }
});
Enter fullscreen mode Exit fullscreen mode

This function calls the DynamoDB PutItem 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 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:

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
}
Enter fullscreen mode Exit fullscreen mode

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.

$ asml cast
$ asml bind
Enter fullscreen mode Exit fullscreen mode

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:

curl --request GET \
  --url http://path_to_your_api/ \
  --header 'authorization: Bearer <TOKEN>'
Enter fullscreen mode Exit fullscreen mode

Your API endpoint URL can be found in the AWS API Gateway console. This endpoint corresponds to the data service. Our put function is at the path (and verb) we defined in the service definition TOML:

curl --request PUT \
  --url https://<api-id>.execute-api.<region>.amazonaws.com/put \
  --header 'authorization: Bearer <TOKEN>'
Enter fullscreen mode Exit fullscreen mode

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 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!

Discussion (0)