DEV Community

Cover image for Building Auth0 Actions in TypeScript
Emma Moinat
Emma Moinat

Posted on

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:

Comparison between Rules, Hooks and Actions

It feels like Actions has it all! Actions offers 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 to discuss Actions and how to handle deploying them from Typescript, we won't delve too deep into the pros and cons of different IaC providers. However, to just mention a few of your options, 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:

Post Login Action Editor

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

Drag and drop workflow

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

However, inline code is not ideal for many reasons, some of these are:

  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.

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

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.

TypeScript to JavaScript

Auth0 Actions are very specific in how the code needs to look so finding the right Rollup config (rollup.config.js) took some time but with perseverance we got there:

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

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.');
  }
};
Enter fullscreen mode Exit fullscreen mode

Unfortunately Auth0 currently lacks public type definitions for Event and PostLoginAPI, so I've implemented custom types. 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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"]
  }
}

Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)