Building Auth0 Actions in TypeScript

Auth0 Actions are the successor to Auth0 Rules and Hooks, the platform's standard way of enabling custom code execution. Actions were introduced to simplify "custom workflows for extending Auth0" and provide "an improved developer experience from Rules and Hooks". In this post, I will show how to build an Auth0 Action using TypeScript and a Terraform provider.

Building Auth0 Actions in TypeScript

Auth0 Actions

In 2022 Auth0 announced Auth0 Actions as the successor to both Auth0 Rules and Hooks.

Auth0 Actions provides a unified view across secure, tenant-specific, self-contained functions that allow you to customize the behavior of Auth0. Each action is bound to a specific triggering event on the Auth0 platform, which executes custom code when that event is produced at runtime. Auth0 blog post about GA of Actions

If you have used or are still using Rules or Hooks you will likely have experienced their limitations. Here is a quick comparison of Hooks and Rules with Actions:

It feels like Actions have it all! Actions offer a smooth developer experience, thanks to its intuitive drag-and-drop flow editor, powerful Monaco code editor and handy version control. Well done Auth0!

Infrastructure as Code (IaC)

While online code editors and drag-and-drop flow editors are convenient, they often fall short when it comes to complex deployments. We need the ability to deploy Auth0 configurations repeatedly, perhaps to different tenants or accounts. Enter IaC.

As this blog post is mostly about Actions and how to deploy them using Typescript, we won't delve too deep into the pros and cons of different IaC providers. Only to say that if you love CDK, then there is a great construct here. Otherwise, there is a Terraform provider here.

In our case, we are using the Terraform provider but most of the following steps you can also make use of in the case of the CDK construct.

Implementing An Action

Let's take an example of the Post Login action, and imagine we want to deny someone access if their email is not verified.

Approach 1: Auth0 UI

Editing in the UI you would create something like this:

Auth0 UI

With the drag-and-drop workflow looking like this:

Auth0 UI 2

Approach 2: Terraform Inline Code

We want to reproduce this configuration in our IaC. Out of the box you can write inline code as follows:

resource "auth0_action" "post_login_action" {
  name    = "PostLoginAction"
  runtime = "node18"
  deploy  = true
  code    = <<-EOT
  /**
   * Handler that will be called during the execution of a PostLogin flow.
   *
   * @param {Event} event - Details about the user and the context in which they are logging in.
   * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
   */
   exports.onExecutePostLogin = async (event, api) => {
     if (!event.user.email_verified) {
        api.access.deny('Please verify your email address to continue.');
     }
   };
  EOT

  supported_triggers {
    id      = "post-login"
    version = "v3"
  }
}

However, inline code is not ideal for many reasons:

  1. Maintainability: As the codebase grows, maintaining large blocks of inline code within Terraform configurations can become cumbersome. It makes the Terraform files longer and harder to read, increasing the risk of errors.

  2. Version Control: Managing code across different environments is easier when it's stored in separate files with proper version control. Inline code makes it difficult to track changes independently from the Terraform configuration.

  3. Reusability: Inline code is tied to a specific Terraform resource, limiting its reuse across multiple resources or projects. External files can be imported wherever needed, promoting code reuse.

  4. Testing: Inline code is harder to test in isolation. By keeping the code in separate files, you can easily run unit tests and other checks before deploying it through Terraform.

  5. Type Safety and Tooling: When using TypeScript or other languages that compile to JavaScript, you benefit from type checking, better editor support, and more robust development tools. Inline JavaScript in Terraform doesn't allow for this enhanced development workflow.

External files

Using external files for your code, with Terraform's file function or similar, resolves these issues by separating concerns, improving maintainability, and allowing better integration with development tools.

Thankfully terraform offers a file helper function. With this helper you simply pass in a path and it will go and pull the contents of that file in as a string.

With this file helper you could then target a JavaScript file as follows:

resource "auth0_action" "post_login_action" {
  name    = "PostLoginAction"
  runtime = "node18"
  deploy  = true
  code    = file("${path.module}/path/to/post-login-action.js")

  supported_triggers {
    id      = "post-login"
    version = "v3"
  }
}

This is also a valid approach, but what we really want is to be able to write our code in TypeScript and have it transpile into the JavaScript we need. Enter Rollup.

Rollup - TypeScript to JavaScript

Auth0 Actions are very specific in how the code needs to look, but with a little perseverance we found the right Rollup config (rollup.config.js):

import typescript from '@rollup/plugin-typescript';

export default {
  input: ["src/post-login-action.ts"],
  output: {
    strict: false,
    format: "cjs",
    dir: "dist",
  },
  external: [], // here you can add any external dependencies
  plugins: [
    typescript({ module: 'es6' })
  ]
}

Note that you will of course need to install both rollup and @rollup/plugin-typescript using your package manager.

Thanks to a thread in the Auth0 Community for this config.

This now enables us to write (and test 😍) the following code:

type PostLoginAPI = {
  access: { deny: (message: string) => void };
};

type Event = {
  user: { email_verified: boolean };
  client: { name: string };
};

export const onExecutePostLogin = async (event: Event, api: PostLoginAPI) => {
  if (!event.user.email_verified) {
    api.access.deny('Please verify your email address to continue.');
  }
};

Unfortunately Auth0 currently lacks public type definitions for Event and PostLoginAPI, so I had to implement custom types for these. I hope Auth0 will release these types in the future, as they would greatly simplify and enhance the type safety of this code.

Once your code is ready you can run rollup -c to transpile your TS code. Finally your terraform resource definition would point to your dist folder where the JS code will be output to:

resource "auth0_action" "post_login_action" {
  name    = "PostLoginAction"
  runtime = "node18"
  deploy  = true
  code    = file("${path.module}/path/to/dist/post-login-action.js")

  supported_triggers {
    id      = "post-login"
    version = "v3"
  }
}

So that's it, your TypeScript code is ready to be deployed as Auth0 Actions.

Learn how to develop AWS serverless applications in TypeScript

Related Insight

Learn how to develop AWS serverless applications in TypeScript

A 3-day, intensive introduction into the world of Serverless and TypeScript

Considerations

You need to ensure your Action code is built before you attempt a terrform plan or apply. In our case we are using terragrunt which has a helpful before_hook, setup in the terragrunt.hcl file as follows:

terraform {
  ...

  before_hook "before_hook" {
    commands     = ["apply", "plan"]
    execute      = ["bash", "../path/to/pre-build.sh"]
  }
}

Where pre-build.sh is a simple script that runs our Action's build command, in our case npm run build.

There are other options out there for pre-plan or pre-apply hooks, it is not essential that you use terragrunt, although, I do recommend checking it out.

Summary

In summary, Auth0 Actions bring a powerful upgrade to the way we customise and extend Auth0, replacing the older Rules and Hooks with a more flexible and unified platform.

By leveraging TypeScript and tools like Rollup, we can maintain type safety and modular code while deploying through Infrastructure as Code (IaC) solutions like Terraform. This approach not only enhances our ability to manage complex deployments but also improves the overall developer experience.

With Actions, you can efficiently create, test, and deploy secure, tenant-specific functions, making it easier to tailor Auth0 to your specific needs. If you have any questions, please leave a comment.

Thanks for following along!

(PS This post was also published on Dev Community, https://dev.to/emmamoinat/building-auth0-actions-in-typescript-20p0)

Article By
blog author

Emma Moinat

Senior Software Engineer