Auth0 as the authentication provider for single-page web applications using SurrealDB as the only backend.
Depending on the version of SurrealDB that you are on: v1.x
or v2.x
, you may have different options available with respect to using Scope and Token or DEFINE ACCESS methods to you for integrating Auth0 as an authentication provider.
In this guide you will learn how to:
v1.x
.This guide will cover the most general case, in which SurrealDB is the only backend for your application. You can still follow this guide even if you have additional backends, but in that case you may have other options available to request and validate tokens issued by Auth0. Likewise, even if your application is not strictly a Single-Page Application (SPA), you may still follow and benefit from this guide.
This guide assumes the following:
You have a fresh instance of SurrealDB running.
You can use a local Docker container without volumes for the purposes of this guide.
docker run --rm --pull always -p 8000:8000 surrealdb/surrealdb:latest \ start --user root --pass root
To run the SurrealQL statements mentioned in this guide, you will also need an interactive shell.
surreal sql -u root -p root --pretty
You will also need to create an Auth0 account, which can be on the free plan.
First of all, you will need to complete the regular setup for creating a Single-Page Application and an API resource within Auth0. You can do this by following the official Auth0 documentation for your SPA.
If you are using plain JavaScript, follow the vanilla guide to create an application in Auth0 and the API guide to create an API in Auth0.
NoteYou will not need to create an actual backend API (e.g. using backend languages like NodeJS or Go) as the documentation suggests when using SurrealDB. A simple file server (e.g.
python3 -m http.server 8080
, for local testing or any static web server for production) that can serve the static content of your website will suffice. However, creating an API resource in Auth0 is necessary, as it will generate an “audience” string which will be required for Auth0 to add claims to its access tokens.
At the end of those tutorials, you should have both an application and an API created in your Auth0 account and a simple client-side web application that authenticates with Auth0 using those two resources. This website will capture the access token issued by Auth0, which is the token that we are using with SurrealDB. If you were not able to create a working website, you can also use this minimal example created by SurrealDB.
When completing the actions above, make sure to keep the following information handy:
Now, Auth0 is ready to perform authentication and issue tokens for your application. However, SurrealDB expects these tokens to contain some specific claims. Auth0 allows adding custom claims through its “actions” and “flows” features. Since Auth0 requires claims for an API audience to be namespaced, these claims will need to have the https://surrealdb.com/
prefix.
To add custom claims, you must create an Auth0 action with the “Login / Post Login” trigger.
For this example, you can use the following code:
exports.onExecutePostLogin = async (event, api) => { if (event.authorization) { // The claims in this block are expected by SurrealDB. // These values should match your SurrealDB installation. api.accessToken.setCustomClaim(`https://surrealdb.com/ns`, "test"); api.accessToken.setCustomClaim(`https://surrealdb.com/db`, "test"); // This value corresponds to the name of the JWT access method // which will be created in SurrealDB during the next section. api.accessToken.setCustomClaim(`https://surrealdb.com/ac`, "auth0"); // In this block, we will add additional claims which are not required by SurrealDB. // These claims can be used from SurrealQL to implement application logic. // In this example, we will add the data that we will store for each user. // We will also use some of this data to perform authorization. api.accessToken.setCustomClaim(`https://surrealdb.com/email`, event.user.email); api.accessToken.setCustomClaim(`https://surrealdb.com/email_verified`, event.user.email_verified); api.accessToken.setCustomClaim(`https://surrealdb.com/name`, event.user.name); api.accessToken.setCustomClaim(`https://surrealdb.com/nickname`, event.user.nickname); api.accessToken.setCustomClaim(`https://surrealdb.com/picture`, event.user.picture); } };
exports.onExecutePostLogin = async (event, api) => { if (event.authorization) { // The claims in this block are expected by SurrealDB. // These values should match your SurrealDB installation. api.accessToken.setCustomClaim(`https://surrealdb.com/ns`, "test"); api.accessToken.setCustomClaim(`https://surrealdb.com/db`, "test"); // These values correspond to the names of the SCOPE and TOKEN resources // which will be created in SurrealDB during the next section. api.accessToken.setCustomClaim(`https://surrealdb.com/sc`, "user"); api.accessToken.setCustomClaim(`https://surrealdb.com/tk`, "auth0"); // In this block, we will add additional claims which are not required by SurrealDB. // These claims can be used from SurrealQL to implement application logic. // In this example, we will add the data that we will store for each user. // We will also use some of this data to perform authorization. api.accessToken.setCustomClaim(`https://surrealdb.com/email`, event.user.email); api.accessToken.setCustomClaim(`https://surrealdb.com/email_verified`, event.user.email_verified); api.accessToken.setCustomClaim(`https://surrealdb.com/name`, event.user.name); api.accessToken.setCustomClaim(`https://surrealdb.com/nickname`, event.user.nickname); api.accessToken.setCustomClaim(`https://surrealdb.com/picture`, event.user.picture); } };
This action should be saved and added to the “Login” flow in the “Actions > Flows” section.
For this simple example, we will create a single table named “user”, where any user that authenticates through Auth0 using your application will be granted complete permissions over their data. For this to work as intended, we will need to ensure that the email address is unique between users and that users are granted permissions to access their own record as long as they authenticated with the access method that we will define.
DEFINE TABLE user SCHEMAFULL -- Authorized users can select, update, delete and create user records. -- Records that do not match the permissions will not be modified nor returned. PERMISSIONS FOR select, update, delete, create WHERE -- The access method must match the method that we will define. $access = "auth0" -- The record identifier must match that of the authenticated user. AND id = $auth ; -- In this example, we will use the email as the primary identifier for a user. DEFINE INDEX email ON user FIELDS email UNIQUE; DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value); -- We define some other information present in the token that we want to store. DEFINE FIELD name ON user TYPE string; DEFINE FIELD nickname ON user TYPE string; DEFINE FIELD picture ON user TYPE string;
Next, we should configure SurrealDB so that it can verify tokens sent to it through the HTTP REST API via the “Authorization” header or through any of the SDKs via the “Authenticate” methods.
To do that, we will leverage the JWKS support in SurrealDB in order to define a token verification mechanism pointing to a JWKS object served by Auth0. This JWKS object can be found in a dedicated endpoint for your Auth0 domain. Pointing to a JWKS file ensures that token verification will work seamlessly even after rotating the signing keys and that tokens signed with revoked keys will no longer be accepted by SurrealDB. To understand how revocation is handled by SurrealDB, read the JSON Web Key Set documentation under DEFINE ACCESS ... TYPE JWT
.
We will also use the AUTHENTICATE
clause in order to check that any necessary token claims have the expected values before returning the user matching the email address provided by Auth0. This is required because Auth0 has no knowledge of the record identifiers that are used in SurrealDB, so we need to use an identifier that can actually be provided by Auth0 in order to retrieve the corresponding record user.
The following queries will create the required resources to authenticate a token for a record user:
-- Specify the namespace and database that will be used. -- These values should match the custom claims that we configured before. USE NS test DB test; -- Define the public key to verify tokens issued by Auth0 for our application. -- The name of the token should match the custom claim that we configured before. DEFINE ACCESS auth0 ON DATABASE TYPE RECORD -- We verify the token using the public keys hosted by Auth0. WITH JWT URL "https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json" -- We check the token claims and map the email address to a record user. AUTHENTICATE { IF ( -- The JWT specification allows the audience claim to be an array or a string. -- In this example, we ensure that it is provided as an array by Auth0. $token.aud.is_array() -- The audience claim must contain the audience of you application. -- This is the value that you defined when creating the API in Auth0. AND $token.aud CONTAINS "<YOUR_AUTH0_AUDIENCE_VALUE>" -- The audience claim must contain your Auth0 user information endpoint. -- It contains the domain generated when when creating the application in Auth0. AND $token.aud CONTAINS "https://<YOUR_AUTH0_DOMAIN>/userinfo" -- The email address in the token must be verified as belonging to the user. AND $token['https://surrealdb.com/email_verified'] = true ) { -- We return the only user that matches the email address claim found in the token. RETURN SELECT * FROM user WHERE email = $token['https://surrealdb.com/email'] } } ;
In the example above, replace the placeholder with the domain value defined for your Auth0 application.
It is important to not that validating the audience of the token is a requirement of Auth0, other providers may require validating additional claims (e.g. iss
, sub
) to ensure that the token is being used as intended. With Auth0, you can also make use of OpenID Connect scopes, which can be accessed through the scopes
claim via $token.scopes
and contain the OIDC scopes requested by the application and granted by the user, which we will not do for this example.
ImportantIn order to allow SurrealDB to establish a connection with Auth0 to download the JWKS object, you will require running it with the network capability. For the strongest security, provide your specific Auth0 domain when starting SurrealDB with
—allow-net
. For example:—allow-net example.eu.auth0.com
.
Next, we should configure SurrealDB so that it can verify tokens sent to it through the HTTP REST API via the “Authorization” header or through any of the SDKs via the “Authenticate” methods.
To do that, we will leverage the JWKS support in SurrealDB in order to define a token verification mechanism pointing to a JWKS object served by Auth0. This JWKS object can be found in a dedicated endpoint for your Auth0 domain.
Pointing to a JWKS file ensures that token verification will work seamlessly even after rotating the signing keys and that tokens signed with revoked keys will no longer be accepted by SurrealDB.
To understand how revocation is handled by SurrealDB, read the JSON Web Key Set documentation under DEFINE TOKEN
.
The following queries will create the required resources to authenticate a token for a scope:
-- Specify the namespace and database that will be used. -- These values should match the custom claims that we configured before. USE NS test DB test; -- Define the scope where the token will be used. -- The name of the scope should match the custom claim that we configured before. DEFINE SCOPE user; -- Define the public key to verify tokens issued by Auth0 for our application. -- The name of the token should match the custom claim that we configured before. DEFINE TOKEN auth0 ON SCOPE user TYPE JWKS VALUE "https://<YOUR_AUTH0_DOMAIN>/.well-known/jwks.json";
In the example above, replace the placeholder with the domain value defined for your Auth0 application.
ImportantIn order to allow SurrealDB to establish a connection with Auth0 to download the JWKS object, you will require running it with the network capability. For the strongest security, provide your specific Auth0 domain when starting SurrealDB with
—allow-net
. For example:—allow-net example.eu.auth0.com
.
For this simple example, we will create a single table named “user”, where any user that authenticates through Auth0 using your application with a verified email address will be able to register, view and update their data. For this to work as intended, we will need to verify some information in the token claims.
DEFINE TABLE user SCHEMAFULL -- Authorized users can select, update, delete and create user records. -- Records that do not match the permissions will not be modified nor returned. PERMISSIONS FOR select, update, delete, create WHERE -- The token scope must match the scope that we defined. -- The name of the scope should match the scope that we defined before. $scope = "user" -- The audience claim must contain the audience of you application. -- This is the value that you defined when creating the API in Auth0. AND $token.aud CONTAINS "<YOUR_AUTH0_AUDIENCE_VALUE>" -- The audience claim must contain your Auth0 user information endpoint. -- It contains the domain generated when when creating the application in Auth0. AND $token.aud CONTAINS "https://<YOUR_AUTH0_DOMAIN>/userinfo" -- The email claim must match the email of the user being queried. AND email = $token['https://surrealdb.com/email'] -- The email must be verified as belonging to the user. AND $token['https://surrealdb.com/email_verified'] = true ; -- In this example, we will use the email as the primary identifier for a user. DEFINE INDEX email ON user FIELDS email UNIQUE; DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value); -- We define some other information present in the token that we want to store. DEFINE FIELD name ON user TYPE string; DEFINE FIELD nickname ON user TYPE string; DEFINE FIELD picture ON user TYPE string;
It is important to know that validating the audience of the token is a requirement of Auth0, other providers may require validating additional claims (e.g. iss, sub) to ensure that the token is being used as intended. With Auth0, you can also make use of OpenID Connect scopes, which can be accessed through the scopes
claim via $token.scopes
and contain the OIDC scopes requested by the application and granted by the user, which we will not do for this example.
It is also important to note that the $auth
variable accessible from SurrealQL will not contain any values in this case, as it requires the id
claim to be added to the JWT, containing the value of the identifier of a SurrealDB record. For the current example, the $auth
variable will not be necessary.
Now that we have everything ready for Auth0 to generate tokens and for SurrealDB to receive and verify them, we will modify our simple web application to use the token issued by Auth0 to register a new user or to update its information if the user already exists.
Because of the diversity of SDKs that both SurrealDB and Auth0 support, it is difficult to provide examples that will work for everyone. In this guide, we will provide code examples for the most unspecific use case of using the SurrealDB HTTP REST API and Auth0 SPA JS.
This code will run entirely on the client and can be added to the static website that you created while following the Auth0 quick start documentation linked at the beginning of this guide.
With the aforementioned interfaces, we can use the following functions to authenticate users:
// Returns users in that the token is authorized to select. // Should return only the user matching the email in the token. const getUser = async () => { // We fetch an access token from Auth0 with the ID token. const auth0Token = await auth0Client.getTokenSilently(); const response = await fetch(surrealDbConfig.endpoint + "/key/user", { method: "GET", headers: { "Accept": "application/json", "Authorization": "Bearer " + auth0Token } }); return response.json(); }; // Creates a user matching the information in the token. // If the user already exists, updates the existing user with the new data. const createUpdateUser = async () => { // We collect the user data from the Auth0 ID token. const auth0User = await auth0Client.getUser(); // We fetch an access token from Auth0 with the ID token. const auth0Token = await auth0Client.getTokenSilently(); // We define the general query to create or update a user. // We leave the method to be defined later. let query = { body: JSON.stringify({ email: auth0User.email, name: auth0User.name, nickname: auth0User.nickname, picture: auth0User.picture }), headers: { "Accept": "application/json", "Authorization": "Bearer " + auth0Token } }; // We get the user that the token is authorized to access. const surrealDbUser = await getUser(); if (surrealDbUser[0].result.length == 0) { // If a user for the token does not exist, we create the record. console.log("Token user does not exist in database. Creating record."); query.method = "POST"; } else { // If a user for the token already exists, we update the record. console.log("Token user already exists in database. Updating record."); query.method = "PUT"; } // We perform the query and return the created/updated record. let response = await fetch(surrealDbConfig.endpoint + "/key/user", query); return response.json(); };
If you want to see how this code would fit inside the web application that you built, you can view this minimal example created by SurrealDB.
To learn about other potential uses for the SurrealDB token functionality, you can read the [DEFINE ACCESS ... TYPE JWT
] documentation page](/docs/surrealql/statements/define/access/jwt).
In this section, we will provide a few examples of how to configure the application to work with Auth0.
You can view and download a minimal example of an SPA using Auth0 and SurrealDB in this repository.
Using public key cryptography algorithms for signing tokens prevents you from having to store any secrets at all in SurrealDB. However, some SurrealDB administrators may prefer to use HMAC algorithms, which use the same secret to both sign and verify the signature of the JWT. This secret will be stored in SurrealDB when provided as value for the DEFINE ACCESS
statement and its access should be restricted, as it can be used to issue arbitrary tokens which will be trusted by SurrealDB. However, we do not recommend this method over using public cryptography, as the later significantly reduces the burden of creating strong secrets, keeping them secret and managing their lifecycle.
According to the OAuth 2.0 specification (part of OpenID Connect), only confidential (as opposed to public) applications should be allowed to use HMAC algorithms, as it requires being able to keep the secret secure. For this reason, Auth0 will not allow using HMAC algorithms with applications of the SPA (Single-Page Application) type, as they generally would have to store the secret in the client, which is publicly accessible. However, because of the particular case of SurrealDB, the secret will not need to be exposed to the client and will instead be stored in SurrealDB.
Auth0 can support scenarios like these through the option to disable OIDC-conformant authentication. Once disabled, Auth0 will allow selecting HMAC algorithms for SPA. Keep in mind that other options may become available in Auth0 after this change which, if modified without proper care, may compromise the security of your application.
If the choice of using HMAC is made, the rest of the guide can be followed as is, with the exception of specifying the proper HMAC algorithm and placing the secret as the value when defining a token. In Auth0, the secret used to sign using HMAC algorithm corresponds to the Auth0 Client Secret string. The example below shows how a token using the HS256 algorithm can be defined:
-- Define the secret to verify tokens issued by Auth0 for our application. -- The name of the access method should match the custom claim that we configured before. DEFINE ACCESS auth0 ON DATABASE TYPE RECORD WITH JWT ALGORITHM HS256 KEY "<YOUR_AUTH0_CLIENT_SECRET_VALUE>" ;
-- Define the secret to verify tokens issued by Auth0 for our application. -- The name of the token should match the custom claim that we configured before. DEFINE TOKEN auth0 ON SCOPE user TYPE HS256 VALUE "<YOUR_AUTH0_CLIENT_SECRET_VALUE>";