Developer Guide

Overview

When integrating Auth0 Extend into your SaaS product there are two things to consider:

  • The extension authoring experience
  • Invoking the extensions at runtime

This guide starts off with extension execution considerations. At the high level, extension execution is very much like calling a web hook from within a product: all extensions are exposed as an HTTP endpoint with a protocol contract you define. Part of the integration process focuses on providing a usable programming model on top of the protocol, securing the extension endpoints to ensure they can only be called from within your product, and designing isolation scopes for code execution.

The second part of this guide talks about providing users of your product with Auth0 Extend authoring experience of extensions. You will see how you can embed Webtask Editor into your site and customize its look & feel, and allow your users to use the wt-cli command line tool.

Let’s get started.

Identify Extensibility Points

Making your platform extensible starts with identifying the places within the product that your customers will be able to extend or customize. If you are already using web hooks today, you can reuse your list of web hooks as a starting point.

Some examples of extensibility points in a hypothetical CRM system could be:

  • new lead has been created,
  • account information was modified,
  • an e-mail must be composed before it is sent,
  • payment status must be verified.

The first decision you need to make with respect to every extensibility point is about the call pattern it needs to support. Is this a one-way, asynchronous notification or do you expect a synchronous response that may alter further processing within your product?

For example, creation of a new lead may generate a simple invocation of the extension that sends a Slack message or adjusts state of some external system. While this happens processing within your product can continue since it has no dependency on the outcome of extension execution:

Asynchronous extension

In contrast, an extension that formats an e-mail body must complete execution and return the result to your product before processing can continue:

Synchronous extension

The second design decision you need to make is related to the data format and schema for the data your product will sent to and accept as a response from each of the extensibility points. Auth0 Extend supports a high fidelity with the HTTP protocol and allows you full flexibility in deciding what data format to use. Having said that, vast majority or extensibility cases will be well served with JSON format of both the request and response, and JSON will be assumed for the rest of this guide.

Choose Programming Models

Once you have decided about the synchronous/asynchronous call pattern and the specific data format and schema to use for each of the extensibility points, the next step is to select the programming model your users will use to implement the logic of the extension, accept the input data, and send data back to your product.

Node.Js programming model

Auth0 Extend allows your users to write Node.js code to implement extension logic. Each extension is a Node.js module that exports a single JavaScript function. Three basic function signatures are supported out of the box as documented in the Webtask Programming Model. However, Auth0 Extend allows you to step outside of these built-in Webtask programming models to allow extension authoring using arbitrary JavaScript function signatures, domain specific languages like T-SQL or Mustache templates, or to inject middleware services that offer a range of customizations of the programming model. These advanced topics are described in the Middleware section.

In the most common case that satisfies vast majority of extensibility scenarios, an extension would accept JSON on input, and return JSON on output. For this use case, the following built-in function signature is adequate:

module.exports = function (ctx, cb) {
  // JSON object sent in the body of the request is parsed
  // and stored in ctx.body
  var result = {};

  // The result JavaScript object will be serialized as JSON in the response body
  return cb(null, result);
};

In more advanced cases where you want to process non-JSON data formats (for example have the extension return a PNG image), you need to use the low-level programming model that supports the full flexibility of the HTTP protocol:

module.exports = function (ctx, req, res) {
  res.writeHead(200, {'Content-Type': 'image/png'});
  var imageData;
  res.end(imageData);
};

In this low level programming model the extension is provided with the raw request and response object representing the HTTP request and response in Node.js. Note that the extension code in this case is responsible for reading in and parsing the request body, which provides it with the flexibility to accept arbitrary data formats.

Instead of using one of the programming models that comes with Auth0 Extend, you can choose to design your own. For example, if you have an “on account changed” extension in your system that accepts information about the account, account representative, and contact history, and allows for a modified account to be returned, you can choose to offer a tailored programming model for your users:

module.exports = function (account, representative, history, cb) {
  // make changes in account info
  return cb(null, account);
};

Supporting advanced scenarios like this that require custom programming models, DSLs, and more, is addressed in the Middleware section.

NPM Modules

The Node.js code your customers write to extend your platform can utilize all built-in Node.js modules as well as any module from the public NPM registry. When your customers write extension code, they should use require as they would normally, e.g:

var mongo = require('mongodb');

module.exports = function (ctx, cb) { };

However, before executing a webtask that has a dependency on an NPM module, the NPM module must be registered with the platform. Registering NPM modules allows the platform to download and install the NPM module beforehand, ensuring that the dependency is available whenever the webtask executes.

If your customers are using the command line tool, then the tool will read the dependencies listed in the package.json file and ensure that each is registered with the platform. If the package.json file specifies a version range for the module semver, the semver will be resolved to a particular version.

If your customers are using the embedded Extend Editor, then they can specify their dependencies by clicking on the editor ‘Settings’ icon and selecting ‘NPM Modules’.

Settings > NPM Modules

Your customers can then enter the NPM module that their code will take a dependency upon.

Adding an NPM Module

If your customers are not using either the command line tool or the embedded Extend Editor, you will need to ensure your customers NPM modules are registered with the platform by calling the HTTP API.

Choose the Extension Authoring Experience

One of the key benefits of Auth0 Extend is the advanced extension authoring experience you can offer users of your platform. You have a range of options.

The most streamlined authoring experience you can offer is based on the Auth0 Extend web-based editor. Your users can use their browser to edit the code of the extension, define secret parameters (e.g. API keys for external systems) their code uses, test their code with an integrated runner, access real-time logs for debugging, and more. The web-based editor can be used as a stand-alone web application you can direct your users to, or it can be embedded as a component into your own web site. The Hosting the editor section explains both options in detail.

In addition to the web-based editor, Auth0 Extend platform comes with a lower level command line tool called wt-cli. You can offer your users the option of managing extensions using this tool or one you derive from it with functionality specialized for your domain. Using wt-cli requires an additional layer of expertise and familiarity with Webtask concepts from your users and as such is only suitable for a more advanced audience than the web-based editor targets. If you choose the allow your users to use wt-cli, you will need to provide them with instructions on configuring the tool with settings specific to their own account in your system. This is addressed in detail in the Enabling Command Line Tool for Your Users section.

The lowest level experience for managing extensions in your system involves calling the Auth0 Webtask management APIs of your Auth0 Extend installation directly over HTTPS. This level of interaction is adequate if you plan to build custom tools or management experiences for your users that do not leverage the Auth0 Extend editor or wt-cli. To interact with the Auth0 Webtask management APIs you can use any standard HTTP library specific to your environment. If you are calling the APIs from Node.js, you can also use the Node.js SDK Auth0 Extend supports.

Isolation Requirements

If you are operating a multi-tenant SaaS platform and use Auth0 Extend to allow your customers to extend your product with custom code, one key decision you need to make is related to the scope of isolation for executing extensions. Scope of isolation implies the following guarnatees in the Auth0 Extend system:

  1. Two extensions executing in different scopes of isolation are guaranteed to be isolated in terms of in-memory state, access to disk, and execution integrity (e.g. one extension cannot affect the processing of the other extension). They are also guaranteed their fair share of computing resources (CPU, memory).

  2. Two extensions execution within the same scope of isolation may or may not share in-memory state, access to disk, and be able to affect each other’s processing. This allows for a level of processing optimization between extensions (e.g. in-memory connection polling to external systems), but it cannot be relied upon as a guarantee.

For example, if user A and user B of your system both author extension code, you will typically want Auth0 Extend to execute these extensions isolated from one another. This way code of user A cannot access any data or affect processing of user B. On the other hand, if user A authors multiple extensions in your system, it may be not only reasonable but desired to execute both extensions with no isolation so that they can share in-memory data or optimize connection management to external systems.

The question you need to ask yourself in the context of your platform is: what is the concept in my platform which defines a trust or isolation boundary for data and processing? Here are a few possible answers and considerations:

  1. Tenant. For majority of multi-tenant SaaS platforms, a tenant of the platform is at the same time the most granular trust and isolation boundary. A tenant can be a subscriber, an account, an organziation, or even an individual user. As long as all configuration and secret data can be shared across code working on behalf of a specific tenant, but not shared with code working on behalf of other tenants, a tenant is likely your desired scope of isolation for Auth0 Extend extensions.

  2. User. In some multi-tenant systems, a tenant represents a high level account or organization, and there are many users or administrators who are part of it. If your system allows all those users to create their own extensions, and it is important that they are isolated from one another, a user may be the desired scope of isolation.

  3. Staging vs production. If your system differentiates between extensions that execute in a staging vs production environment, it is likely desirable for the execution contexts of these extensions to be isolated. This way a bug that affects processing in the staging environment will not affect execution of extensions in production.

  4. Single extension. In an extreme case, you can say that every extension in the system must be completely isolated from any other extension, without regard for the fact that security of the system may not require that level of granularity. One reason to require this level of isolation is optimizing for stability: ensuring that execution of one extension will never affect execution of other extensions (for example due to an uncaught exception). One disadvantage of this approach is that it prevents optomizations that rely on sharing in-memory data between extensions. Another is related to scalabiltiy of the system (see below).

Choice of the scope of isolation has impact on the scalability and performance of the system: the more granular scope of isolation, the fewer extensions per second you will be able to process given a specific size of an Auth0 Extend deployment. Supporting more isolated environments for extension execution requires more system resources. Scope of isolation set at the tenant level will consume fewer resources than scope of isolation set at user or extension level, assuming there are many users or extensions per tenant.

In most cases, scope of isolation set at tenant level is adequate.

Key Auth0 Extend Concepts

Previous sections of this guide focused on collecting and defining extensibility requirements in the context of your platform. Before moving on to implementing the actual integration to satisfy these requirements, you need to understand the core concepts of Auth0 Extend and the underlying Webtask technology.

  1. Webtask URL. As part of the onboarding process to Auth0 Extend, you received a Webtask URL, which may look like https://wt-yourcompany.it.auth0.com. This is the base URL you will use to invoke management APIs and execute your extensions.

  2. Webtask token. Webtask tokens are used to authorize calls to Auth0 Extend management APIs. Each token has specific set of permissions and intended use. One of the management APIs allows you to create derived webtask tokens with scoped down permissions within your Auth0 Extend deployment. You can read more about the security model and webtask tokens in the next section. Here are the two types of tokens that will be used throughout this guide:

    • Master webtask token. This is the webtask token you were given during the onboarding process into Auth0 Extend. This token gives you complete control over your Auth0 Extend deployment. You can use it to authorize calls to management APIs without any restrictions. NOTE you should never disclose your master webtask token to any of your customers or tenants.

    • Tenant webtask token. This token is scoped down to allow management of all extensions within a single scope of isolation in your platform (typically a single tenant). You can create tenant webtask tokens using your master webtask token. Tenant webtask tokens should only be shared with the relevant tenant.

  3. Extension or webtask. These terms will be used interchangably in this guide to describe the same concept: the atomic unit of custom logic authored by your users that can be separately managed and invoked by your platform. Logically an extension is similar to a single web hook. Extensions are defined by a combination of custom code, secret configuration, and metadata.

  4. Extension code. This is a script that implements custom logic and is authored by users of your platform. In a typical case, it is a single JavaScript function in Node.js. However, with the use of middleware you can allow your users to author extension code that is written in custom, domain specific languages and scripts, e.g. T-SQL for querying your data or a Mustach template for formatting the body of an e-mail.

  5. Extension secrets. These are key/value pairs that your user can define for a particular extension via the Extension Editor, wt-cli, or HTTP APIs. Secrets allow for a secure mechanism to provision exension code with sensitive parameters, e.g. API keys or database connection strings. Auth0 Extend provides a convenient, secure, built-in mechanism for encrypted storage of secrets at rest, and only makes them available to executing extension code in decrypted form at runtime.

  6. Extension metadata. These are key/value pairs that can be used to associate arbitrary metadata with a particular extension. Their primary use is annotating extensions to enable platform-specific functionality written by you (as opposed to your users) in order to provide management experience tailored to your platform. For example, you can use metadata to classify extensions (e.g. “on-new-lead”, “format-email”), or indicate which of them are active vs disabled. Metadata can be used as search criteria when listing available extensions, which is a convenient way to determine whether an extension to call exists at runtime.

  7. Webtask container. Webtask container is a logical, named entity that represents scope of isolation in Auth0 Extend as described in the Isolation requirements section. Extensions executing in different webtask containers are guaranteed to be isolated. Extensions executing in the same webtask container may or may not run in the same address space (process) and therefore share the same state and resources. Webtask containers do not need to be explicitly created or registered - they are created by Auth0 Extend on demand as needed.

  8. Webtasks vs webtask containers. Webtasks are named entities that are created in the context of a specific webtask container. Webtasks must have unique names within their respective containers, but there may be identically named webtasks in different containers. To execute a webtask both the webtask container name and webtask name are needed.

Mapping Isolation Requirements onto Webtask Tokens

After you have identified the desired isolation scope in your system, you can use the webtask containers along with webtask tokens as way of implementing support for it.

At the high level, each isolation scope should be assigned a unique, single webtask container to create and execute extensions in. For each webtask container you will also use the master webtask token to create a unique, derived webtask token with permissions scoped down to just that webtask container.

For illustration, let’s assume your desired isolation scope is a tenant (a subscription or account in your multi-tenant system). Every tenant would then be assigned a unique webtask container name within which all their extensions will execute. Webtask container name can be either derived from the tenant name in your system, or a random, unique name can be used as long as you maintain the association between your notion of a tenant and that webtask container name in Auth0 Extend.

Once the webtask container name for a given tenant is decided, you must use your master webtask token to create a tenant webtask token with permissions scoped down to allow managing only that single webtask container assigned to the tenant. The purpose of the tenant webtask token is to share it with your customer who owns specific tenant. They will use this token with Auth0 Extend editor, wt-cli, or HTTP management APIs of Auth0 Extend to manage their extensions. At the same time, the token does not provide them with permissions to manage extensions of other tenants.

A convenient moment to create a tenant webtask token is during your own tenant provisioning process. Alternatively, you can implement lazy provisioning of tenant webtask tokens (e.g. only when a tenant chooses to add an extension to your system). Regardless when you create the tenant webtask token, you must then durably store the token alongside your tenant (e.g. in your “customers” database) to ensure that you use the same token for a particular tenant every time.

The webtask management API to use to issue a tenant webtask token is POST /api/tokens/issue. The call must be authorized with your master webtask token. The API allows you to specify a number of restrictions for the new token which are outlined in the documentation. The most important to specify is the ten restriction, which limits the management permissions of the new webtask token to the webtask container with a matching name:

POST {host_url}/api/tokens/issue
Content-Type: application/json
Authorization: Bearer {master_webtask_token}

{
  "ten": "{webtask_container_name}"
}

The response to this API call contains the tenant webtask token in the body. Store it alongside your tenant information in your system for future use (or alongside whichever isolation scope you chose to use if other than tenant).

See mapTenantToIsolationScope function.

Creating Extensions

Before your users can edit extensions using Auth0 Extend editor, your platform must pre-create them on their behalf. This is typically done lazily: if a specific extension does not yet exist, your platform must create it immediately before launching the Auth0 Extend editor. You can check if a particular extension already exist by discovering extensions.

Each extension is a webtask that has a name unique within the webtask container, consists of code, secrets, and metadata, and will be callable via HTTPS at a specic URL once created. As such, you must consider the following when pre-creating extensions for your users to edit:

  1. Name. In simple cases, webtask name can act as an identifier of a specific extension type (e.g. “on-new-lead”). Your platform can quickly look up an extension for a specific extensibility point to edit or call based on that name and the webtask container name which is implied by the execution context (e.g. the current tenant).

  2. Code. What is the code of the newly created, “empty” extension? It is a good practice to define this code to have no side effects by default, and contain some minimal documentation in the form of comments that will get your users started when they edit it. For example:

     /**
     @param ctx.body - new lead information
     @param ctx.body.name - name of new lead
     */
     module.exports = function (ctx, cb) {
       var extra_info = {};
       // Add extra information to the lead entry:
       // extra_info.campaign = 'blog';
       return cb(null, extra_info);
     };
    
  3. Secrets. Secrets are typically only needed when the extension code is accessing protected downstream resources. As such the initial list of secrets for an emtpy extension would not specify any application specific secrets. Your users will add them as needed, depending on what their code does.

  4. Metadata. Your platform may rely on metadata for several uses. They are covered in other sections of this guide as needed. Typically, when creating an “empty” extension you will want to add three metadata properties:

    • One that is specific to your platform and declares what the type of the extension is (e.g. “on-new-lead”), which later allows you to search for this extension in order to execute it. NOTE as mentioned before, in simple cases, instead of using this metadata property, you can rely on a convention of naming the extensions in a way that implies their type.

    • Two properties that taken together enable the call to the extension URL to be authorized as originating from your platform. See below.

  5. Authorization. Once created, extension URLs are public. As such it is necessary to ensure that only calls originating from your platform are authorized to call the extension. There are many mechanisms that can be used to achieve this. One simple pattern relies on the extension code requiring that the request specifies a secret token that is only known to your platform and the extension itself. To facilitate this pattern, you must associate a special middleware with the newly created extension. The middleware enforces the authorization decision without requiring any changes to the code your user writes.

To create a webtask that implements an extension following all the considerations above, you can use the PUT /api/webtask/{container}/{name} API as follows:

PUT /api/webtask/{webtask_container}/{webtask_name}
Content-Type: application/json
Authorization: Bearer {tenant_webtask_token}

{
  "code": "{code}",
  "secrets": {
    "auth0-extension-secret": "{secret}"
  },
  "meta": {
    "auth0-extension-secret": "{secret}",
    "auth0-extension-type": "{extensibility_point_name}",
    "wt-compiler": "auth0-ext-compilers/generic"
  }
}

The following parameters must be provided in this call:

  • {webtask_container}: name of the webtask container associated with the isolation scope (e.g. tenant) as determined in Mapping Isolation Requirements onto Webtask Tokens.

  • {webtask_name}: the name of the webtask; in simple cases this uniquely identifies the extensibility point in your platform.

  • {tenant_webtask_token}: tenant-specific webtask token derived from the master webtask token as described in Mapping Isolation Requirements onto Webtask Tokens.

  • {code}: the source code of the “empty” extension as described above.

  • {secret}: a randomly generated secret key that will be used for authorizing calls to the webtask. This should have enough entropy to act as a strong cryptographic key. This can for example be hex encoded 32 byte long array generated with a random number generator. Note the same value must be specified both in metadata and secrets.

  • {extensibility_point_name}: a unique name of this extensibility point type within your platform, e.g. “on-new-lead”. Auth0 Extend does not require this property or associate any semantics with its value. Specifying it is part of a usage pattern that makes it convenient for you to later query and find all extensions defined by your users.

  • wt_compiler: this metadata property specifies a webtask compiler the extension will use. The auth0-ext-compilers/generic compiler (check the source code) is part of the Auth0 Extend platform, and its purpose is to authorize all calls to this webtask by ensuring that the Bearer credential specified in the Authorization header of the request matches the value of the auth0-extension-secret secret. This means your platform will need to correctly formulate the Authorization header when making calls to execute extensions, which is explained in the Invoking Extensions section.

A successful response to this API call will contain the Location HTTP response header with the webtask URL of the extension. You can choose to store it in your database, but it is not necessary to later find out if the user defined an extension for a specific extensibilty point. Thanks to the metadata properties you associated with the extension above, you can use Auth0 Extend management APIs to discover defined extensions.

See ensureExtensionExists function.

Discovering Extensions

There are at least two situations in which you need to determine if a specific tenant in your system has an existing extension of a specific extensibility point:

The same GET /api/webtask/{container} Auth0 Extend management API can be used in both cases. The API allows you to list all extensions that exist in a specific webtask container, and allows you to optionally specify further filtering criteria. You can use this capability to filter the list down to extensions with a specific value of the auth0-extension-type metadata property that stores the name of the extensibility point in your system.

To find out all extensions defined in a specific webtask container (i.e. for a particular tenant), issue:

GET {host_url}/api/webtask/{webtask_container}
Authorization: Bearer {tenant_webtask_token}

To find out all extensions associated with a specific extensibility point in your platform, issue:

GET {host_url}/api/webtask/{webtask_container}?meta=auth0-extension-type:{extensibility_point_name}
Authorization: Bearer {tenant_webtask_token}

The following parameters must be provided in these call:

A successful response to these API calls will contain a JSON array with objects representing matching extensions. For each extension, you will get:

  • name: the name of the webtask implementing this extension.

  • host_url: the HTTPS URL for your Auth0 Extend instance.

  • meta: an object representing all metadata properties of this webtask associated with it at creation time.

See browser discoverExtensions function.
See Node.js discoverExtensions function.

Enabling Command Line Tool for Your Users

Auth0 Extend comes with a command line tool wt-cli that allows for creating and managing webtasks, access to real-time logs from an Auth0 Extend installation, and more. This is a low level tool targeting more sophisticated users (developers and administrators). To use the tool effectively for managing extensions requires your users to have a similar level of understanding of key concepts as presented in this guide.

If you choose to enable your users to use wt-cli, you will need to provide them with instructions for setting up a wt-cli profile specific to the tenant they are allowed to manage. The wt-cli profile is a concept that exists at the level of the wt-cli tool only: it is a combination of configuration paramaters that allows wt-cli to connect to the correct Auth0 Extend installation and invoke management APIs on it. A user of wt-cli can define several named profiles, and select the profile to use when invoking individual commands. A profile consists of:

  • host_url: the HTTPS URL for your Auth0 Extend instance.

  • webtask_token: this should be the tenant webtask token you have created to allow management of extensions at tenant scope. NOTE never disclose your master webtask token to your users.

  • webtask_container: this should be the webtask container name you assigned to the specific tenant - the same container name the tenant webtask token is authorized to perform management operations on.

As you can see, the profile your users will need to set up is tenant-specific. The command your users need to run to set up that profile after installing wt-cli looks like this:

wt init -p {profile-name} \
  --url {host_url} \
  --token {tenant_webtask_token} \
  --container {webtask_container}

If the configured {profile-name} is makaron, your users would then be able to create a hypothetical on-new-lead extensions with:

EXTENSION_SECRET=$(openssl rand 32 -hex)
wt create on-new-lead.js -p makaraon \
  --secret auth0-extension-secret=$EXTENSION_SECRET \
  --meta auth0-extension-secret=$EXTENSION_SECRET \
  --meta auth0-extension-type=on-new-lead \
  --meta wt-compiler=auth0-ext-compilers/generic

The example above corresponds to the management API call to pre-create an extension.

The wt-cli is open source and available on GitHub. You can use it as an inspiration and basis for a higher level command line tool that is specific to your platform, similarly to what we have done to support managing Auth0 Hooks in the Auth0 Identity platform here.

Invoking Extensions

This section explains what you need to do when your platform reaches the point of processing when you need to invoke extension logic.

In all instances, invocation of an extension occurs during an operation or transaction which makes the following parameters implicit:

  • {webtask_container}: name of the webtask container associated with the isolation scope (e.g. tenant) of the current operation or transactionas as determined in Mapping Isolation Requirements onto Webtask Tokens. For example, if your scope of isolation is a tenant of your system, and you are processing a transaction on behalf of that tenant, the webtask container name of the extension to run is already known.

  • {extensibility_point_name}: a unique name of the extensibility point type you want to invoke, e.g. “on-new-lead”.

With this information, you can discover if an extension is defined for a given extensibility point and isolation scope. The discovery process leads to the determination of the following two parameters, both of which are necessary to invoke the extension:

  • {host_url}: this is the url to the specific extension instance to call.

  • {extension_secret}: this is the value of the auth0-extension-secret metadata property you will need to authorize the call.

NOTE invoking the discovery logic every time you need to execute an extension in your system may be impractical from performance perspective, depending on the frequency of extension execution. Consider applying caching mechanism to reduce the overhead of repeated Auth0 Extend management API calls related to discovery.

In addition to the parameters above, you need a payload to supply to the extension, which you defined as part of identifying the extensibility points in your platform.

Auth0 Extend supports creating extensions that leverage full flexibility of HTTP, including support for different verbs and content types. In the most common case of using JSON to submit payload to the extension, an invocation of the extension would look like this:

POST {host_url}
Content-Type: application/json
Authorization: Bearer {extension_secret}

{payload}

A typical success response would have HTTP 200 status code and contain a JSON response body generated by the extension code. Your platform is reponsible for performing any necessary validation of the status code, and payload.

See invokeExtension function.

The errors you may receive from calling Auth0 Extend extensions fall into several catergories and are described in Handling Errors from Extensions.

One of the implementation decisions you need to make when invoking the extensions is related to their synchronous or asynchronous semantics, which you determined when identifying the extensibility points in your platform. When invoking synchronous extensions you must wait for the HTTPS request to complete before your platform can continue processing, because it has a dependency on the data the extension will respond with. In such cases you should consider enforing a timeout on the overall duration of extension execution to ensure users of your platform have a good experience.

When invoking asynchronous extensions, transaction processing in your platform will continue as soon as the HTTPS request to the invoke the extension has been issued. One consideration you should make in this case is related to handling of errors and output from asynchronous extensions. To aid in troubleshooting and monitoring, it may be reasonable to capture and log the outcome of asynchronous extension execution, even though it does not affect the processing of the transaction that triggered it.

Handling Errors from Extensions

Successful execution of an extension typically results in a HTTP 200 response with extension-specific payload in the response body. If you receive a non-200 response, the following information will guide you in understanding the error condition.

All responses from calling extension URLs contain the x-wt-response-source HTTP header. It indicates the component of the Auth0 Extend stack that generated the response. Given this information, the response status code, and the response payload, you can decide how to handle it. These are the possible values of x-wt-response-source:

  • webtask: the response was generated as a result of executing the code of the extension. You will see this value when the extension completed successfuly but also when it returned an error object through the callback function, or the code of the extension generated an uncaught exception. You can differentiate between success and failure using the status code of the response and the content of the response body.

  • compiler: the code of the extension could not be compiled. The most likely cause is a syntax error in the code. In those cases the body of the response contains more information about the location of the error.

  • proxy, network: these uncommon values indicate an error that occured in Auth0 Extend infrastructure before the execution of the extension code. Typically there will be more details in the body of the response.

Below are the most common cases of responses you will see.

Successful Response

An extension that returns a valid response can look like this:

module.exports = (ctx, cb) => {
  cb(null, { valid: 'response' });
};

The response you will see when calling this extension is:

HTTP/1.1 200 OK
Content-type: application/json
x-wt-response-source: webtask

{"valid":"response"}

Application Level Error

An extension that explicitly returns an application error:

module.exports = (ctx, cb) => {
  cb(new Error('Some error'));
};

The response you will see when calling this extension is:

HTTP/1.1 400 Bad Request
Content-type: application/json
x-wt-response-source: webtask

{
  "code": 400,
  "error": "Script returned an error.",
  "details": "Error: Some error",
  "name": "Error",
  "message": "Some error",
  "stack": "..."
}

Application Level Error with Custom Status Code

An extension that explicitly returns an application error can override the default HTTP 400 status code with a custom status code value:

module.exports = (ctx, cb) => {
  var error = new Error('Some error');
  error.statusCode = 401;
  cb(error);
};

Response:

HTTP/1.1 401 Unauthorized
Content-type: application/json
x-wt-response-source: webtask

{
  "code": 401,
  "error": "Script returned an error.",
  "details": "Error: Some error",
  "name": "Error",
  "message": "Some error",
  "stack": "..."
}

Syntax Error in Extension Code

An extension that cannot be compiled due to a syntax error:

module.exports = (ctx, cb) => {
  random text
};

Response:

HTTP/1.1 400 Bad Request
Content-type: application/json
x-wt-response-source: compiler

{
  "code": 400,
  "message": "Compilation failed: Unexpected identifier",
  "error": "Unexpected identifier",
  "stack": "SyntaxError: Unexpected identifier\n    at ...""
}

Uncaught Synchronous Exception in Extension Code

An extension that generates an uncaught synchronous exception:

module.exports = (ctx, cb) => {
  throw new Error('Some error');
};

Response:

HTTP/1.1 500 Internal Server Error
Content-type: application/json
x-wt-response-source: webtask

{
  "code": 500,
  "error": "Script generated an unhandled synchronous exception.",
  "details": "Error: Some error",
  "name": "Error",
  "message": "Some error",
  "stack": "..."
}

Uncaught Asynchronous Exception in Extension Code

Uncaught asynchronous exceptions thrown by extension code require careful consideration. Such exceptions lead to the termination of the process in which the extension is running. Depending on the isolation scope you have selected, there may be other extensions executing in that process at the same time. For example, if a single tenant defined multiple extensions, and there are multiple concurrent transactions of that tenant which require execution of extensions, an uncaught exception generated by one execution will termite the execution of all other concurrently executing extensions in the same process. Given that, if you receive the “Uncaught asynchronous exception” error, it may as well indicate a problem in the code or exection of another extension.

This is an extension that generates an uncaught asynchronous exception:

module.exports = (ctx, cb) => {
  setTimeout(() => {
    throw new Error('Some error');
  }, 1000);
};

Response:

HTTP/1.1 500 Internal Server Error
Content-type: application/json
x-wt-response-source: webtask

{
  "code": 500,
  "error": "Script generated an unhandled asynchronous exception.",
  "details": "Error: Some error",
  "name": "Error",
  "message": "Some error",
  "stack": "..."
}

Runtime and Management APIs

Auth0 Extend exposes the following APIs for Runtime and Management.

Monitoring and Diagnostics

There are several tools and practices to achieve the level of monitoring for Auth0 Extend which provides you with insight into how your users are using extensions and enable you to offer them support, as well as allow them to diagnose issues themselves.

Your Auth0 Extend installation can be configured to export logging information to your own AWS Firehose service. This provides you with the capability to store and analyze logs generated by your users’ extensions in Amazon S3, Amazon ElasticSearch, Amazon Kinesis Analytics, or Amazon Redshift. The logging information contains all output generated by your user’s extension code to standard output, as well as selected infrastructure events generated by Auth0 Extend itself. Contact support if you are interested in configuring this capability.

For development and ongoing support of your customers, you can leverage access to real-time logs. These logs can be made available to your customers via the Extend editor to support their debugging and development by enabling the logging panel via the allowAccessingLogs setting.

logging

One way to conveniently stream real time logging information is to use the wt-cli command line tool. Before you can use it, you need to configure a profile based on your master webtask token - this will allow you to connect to the webtask container of any of your customers:

wt init -p master \
  --url {host_url} \
  --token {master_webtask_token} \
  --container master

You can then use the wt logs command to attach to any webtask container and stream logging information generated by any of the extensions running in it:

wt logs -p master --container {tenant_webtask_container}

The wt-cli tool can also be used to inspect the source code of the extensions your customers have created. Depening on the level of support you choose to provide your customers, this may be a capability that can save your support team a lot time and effort. To list extensions defined in a specific webtask container, run:

wt ls -p master --container {tenant_webtask_container}

(note that the result is paged).

Next, to obtain the code of a specific extension from the list, run:

wt inspect -p master --container {tenant_webtask_container} {webtask_name} --fetch-code

NOTE never share the master webtask token with your customers.

There are a few good practices practices to follow to improve your diagnostics capabilities when integrating Auth0 Extend into your platform; these include:

  • Log extension invocations. Every time you invoke Auth0 Extend extension, it is worthwhile to capture the following information in the logs of your platform:

    • The overal execution time of the extension.
    • The response status code.
    • The following response headers: x-wt-response-source, x-auth0-proxy-stats, s-auth0-stats
    • Consider if and which parts of the response to capture in logs, since there may be sensitive information embedded in the response.
  • Log extension discovery calls. Every time you call the extension discovery, it is worthwhile to capture the entire response in your logs (after removing any sensitive information, e.g. webtask tokens).

Middleware

Auth0 Extend middleware is custom Node.js code you control that executes right before the extension code your users write. It allows you to augment the default extension execution logic supported by Auth0 Extend without requiring any changes in the code of your users.

Most common situations where middleware is useful include:

  1. Custom programming models. With middleware, you can transform and adapt webtask function signatures to offer custom programming models for your users. For example, the on-new-lead extension could offer a function (new_lead, history, context, callback) function signature that is specific to the domain it addresses. However, this adaptation is not limited to JavaScript: you can as well support writing extensions using domain specific languages like T-SQL or a templating engine.

  2. Authorization. Middleware allows you to implement authorization and access control logic for calls to extension endpoints. For example, the Creating Extensions section recommends using a pattern of securing extension endpoints that builds on middleware.

  3. Logging. Middleware allows you to build a custom logging solution. For example, you can intercept and capture all calls to console.log made by extension code to filter or export it to an external system.

  4. Context enhancement. You can use middleware to provide extension code with additional data or functionality beyond what Auth0 Extend provides by default. For example, you can pre-fetch data from a database or make a function that fetches data available for calling from the extension code.

Middleware for an extension is determined via the wt-compiler metadata property of a webtask, which sets a webtask compiler for the webtask. Normally it is specified at the time the extension is created. You can read more about creating and setting custom webtask compilers here.

NOTE The Creating Extensions section recommends using the auth0-ext-ompilers/generic for the value of wt-compiler metadata property. This middleware enforces a simple authorization scheme to ensure that only your platform can execute extensions. If you choose to set your own middleware, you need to make sure adequate replacement for that authorization mechanism is in place. You can refer to the code of the auth0-ext-compilers/generic implementation here.

Let’s look at an example of a custom middleware that introduces a domain specific programming model and enforces the same authorization mechanism as auth0-ext-compilers/generic. Imagine you want your users to be able to modify the logic of creating a new lead in a hypothetical CRM system. When a new lead is created they will be notified and get a chance to add extra attributes to the new lead record.

The body of the request to this extension would be a JSON object that looks like this:

{
  "name": "Jon Doe",
  "value": 1000
}

The expected body of the response would contain a JSON object with new attributes to add to the record, e.g.:

{
  "newAttributes": {
    "vip": true,
    "profile": {}
  }
}

Out of the box, this is the programming model of an Auth0 Extend extensions given this schema:

module.exports = function (ctx, cb) {
  var newAttributes = {};
  if (ctx.body.value > 500) {
    newAttributes.vip = true;
  }
  return cb(null, {
    newAttributes: newAttributes
  });
};

Now assume you want to provide your users with a much more friendly and self-exlanatory programming model:

module.exports = function (name, value, cb) {
  var newAttributes = {};
  if (value > 500) {
    newAttributes.vip = true;
  }
  return cb(null, newAttributes);
};

The way to achieve this is to implement a webtask compiler which adjusts between the two programming models:

module.exports = (options, cb) => {
    // First compile the user-supplied extension code into a function
    options.nodejsCompiler(options.script, (error, func) => {
      if (error) return cb(error);
      // Now return a function that uses the (ctx, cb) programming model
      // supported natively by Webtasks, and adjusts the programming models
      // when called
      return cb(null, (ctx, cb) {
        // First, enforce authorization check similar to auth0-ext-compilers/generic.
        var match = (ctx.headers['authorization'] || '').trim().match(/^bearer (.+)$/i);
        if (!match || match[1] !== ctx.secrets['auth0-extension-secret']) {
          var error = new Error('Unauthorized call');
          error.statusCode = 401;
          return cb(error);
        }

        // Extract inputs from request body into stand-along arguments,
        // wrap callback to transform response format.
        var name = ctx.body.name;
        var value = ctx.body.value;
        return func(name, value, (error, response) => {
          if (error) return cb(error);
          // Transform the response format
          return cb(null, {
            newAttributes: response
          });
        })
      })
    });
};

You need to host the code of the compiler at a place where it can be obtained over HTTPS, say https://cdn.mycompany.com/on-new-lead-compiler.js.

Now, when creating a new on-new-lead extension, you can refer to this compiler instead of the auth0-ext-compilers/generic one:

PUT /api/webtask/{webtask_container}/{webtask_name}
Content-Type: application/json
Authorization: Bearer {tenant_webtask_token}

{
  "code": "{code}",
  "secrets": {
    "auth0-extension-secret": "{secret}"
  },
  "meta": {
    "auth0-extension-secret": "{secret}",
    "auth0-extension-type": "on-new-lead",
    "wt-compiler": "https://cdn.mycompany.com/on-new-lead-compiler.js"
  }
}