Building Auth0 Actions in TypeScript
05 September 2024
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.
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:
With the drag-and-drop workflow looking like this:
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:
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.
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.
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.
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.
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.
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)