SurrealDB University is LIVE! Master the future of data

Gradient
SurrealDB University

Authentication Required

This course requires authentication.
Please sign in to continue

Background Gradient
Next
Back to Courses

Authentication

There are multiple forms of authentication built into SurrealDB, supporting different use cases.

In this lesson, we’ll cover:

  • System user authentication
  • Record user authentication
  • Sessions and expiration
  • Query Safety

System users

System users are users defined directly on SurrealDB by a system user with the owner role, using the DEFINE USER statement.

System users are assigned a level (root, namespace or database) to limit what they can do to the system.

  • Root owners have access to (and can create users on) all namespaces and databases.
  • Namespace owners have access to (and can create users on) their specific namespace and every database in it
  • Database owners have access to (and can create users on) their specific database.
DEFINE USER admin ON ROOT PASSWORD 'VerySecurePassword!' ROLES OWNER; DEFINE USER john ON NAMESPACE PASSWORD 'VerySecurePassword!' ROLES OWNER; DEFINE USER jane ON DATABASE PASSWORD 'VerySecurePassword!' ROLES OWNER;

In this example we’re creating a user for each of the levels, root, namespace and database with a “very secure password” and assigning the OWNER role.

RETURN crypto::argon2::generate('VerySecurePassword!'); DEFINE USER gordon_freeman ON DATABASE PASSHASH '$argon2id$v=19$m=19456,t=2,p=1$0kz5uZcfFg/VPHoDEGYgbQ$8HBIZXSOYzZ2qickKAhxwenvILoiWuockJvB8Ma4Stg' ROLES EDITOR;

If you are required to store passwords for your users, don’t rely on table or field permissions to keep them private. In the event that your application or database is compromised, these passwords would become known by the attacker. Instead, use the password hashing functions provided by SurreaDB such as argon2. These functions ensure that irreversible cryptographic hashes are stored instead of the original passwords, so that the passwords from your users remain safe even in the event of a compromise.

In this example we’re generating a argon2 passhash then using it in our user definition and assigning the editor role.

Role Based Access Control

Along with the different levels, SurrealDB implements Role Based Access Control (RBAC) to further define what a user can do. Each user is assigned one or more roles (currently limited to the built-in OWNER, EDITOR and VIEWER roles) and will be allowed to perform an action over a resource as long as at least one of their roles allows it.

The OWNER and EDITOR roles can view and edit any resource on the user’s level or below. However, only the OWNER role can create users and other IAM resources such as access methods.

The VIEWER role can only view any resource on the user’s level.

Least Privilege

When assigning roles it’s best practice to ensure that you employ the principle of least privilege and create users at the lowest level possible and with the minimum role in order to be able to perform their duties inside of SurrealDB. This will mitigate some of the risk in the case where credentials for that user are ever compromised.

DEFINE USER db_viewer ON DATABASE PASSWORD 'CHANGE_THIS' ROLES VIEWER;

An example of this is assigning the VIEWER permission to a user which only needs to perform read-only queries to the database.

Record users

Record users represent users that are defined as a record in a database instead of through the DEFINE USER statement. Since these users exist as regular database records, they can have associated fields containing any information required for authentication and authorisation.

Thanks to this, SurrealDB is able to offer mechanisms to define your own SIGNIN and SIGNUP logic as well as custom table and field permissions for record users. This feature contributes to making SurrealDB an all-in-one Backend-as-a-Service (BaaS).

Record users are defined with the DEFINE ACCESS statement of TYPE RECORD.

A record access is configured with the following specific clauses:

  • SIGNUP: Defines the logic for when a user signs up as a record user and usually creates a new record in a table.
  • SIGNIN: Defines the logic for when a user signs in as a record user and usually checks credentials against table records.
  • AUTHENTICATE: Can be used to change the record identifier returned by the SIGNUP and SIGNIN clauses. If no record identifier is returned, it will return an error. The AUTHENTICATE clause can also be used to log or stop authentication attempts from record users, as it is always executed across the SIGNUP and SIGNIN clauses.

By default, record users have no permissions. They don’t use the Role-Based Access Control (RBAC) system and can only access data if allowed by a PERMISSIONS clause, which is defined on every data resource for example tables and fields and defaults to NONE.

Record authentication example

Let’s go over one of the many ways you can set up record authentication. Given you can define your own logic, there is not a single way to do it. Feel free to modify where needed!

Define a user table and fields

Typically, you would define a user table where new records are created every time a user signs up.

DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id; DEFINE FIELD name ON user TYPE string; DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value); DEFINE FIELD password ON user TYPE string; DEFINE FIELD enabled ON user TYPE bool; DEFINE INDEX email ON user FIELDS email UNIQUE;

For this example we first define the user table and a few fields that enforce the following:

  • An authenticated user can select, update and delete its own user record.
  • Asserts that the email provided by the user is actually an email address.
  • Forbid users to use an email that is already in use by another user. We do this by creating a unique index for the email field.

Define the user record access

DEFINE ACCESS user ON DATABASE TYPE RECORD SIGNUP ( CREATE user CONTENT { name: $name, email: $email, password: crypto::argon2::generate($password), enabled: true } ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) ) AUTHENTICATE { IF !$auth.enabled { THROW 'This user is not enabled'; }; RETURN $auth; };

Then we define the user record access. This allows users to SIGNUP and SIGNIN by selecting or creating a record in the table defined in the previous step.

We will configure the record access like this:

  • The SIGNUP logic needs the name, email and password parameters to be provided by the user. In the query, we can use them as $name, $email and $password.
  • The SIGNIN logic needs the email and password parameters to be provided by the user. In the query, we can use them as $email and $password.

The optional AUTHENTICATE clause is a good fit for validating specific conditions that are not expected to change during the lifetime of the session. Such as our example of validating if a user is enabled or not. If the user in not enabled, we can use THROW to return a custom error message. If we don’t return anything, we’ll just get a generic authentication error.

To learn more about creating a record user, refer to the DEFINE ACCESS … TYPE RECORD documentation.

Sessions

Whenever authentication is performed with any kind of user against SurrealDB, a session is established between the client and the SurrealDB server with which the connection was established. These sessions exist only in memory on the server for the duration of the connection, whether it is a single request through the HTTP REST API or through multiple requests in the same connection using the WebSocket API and any of the SDKs that leverage it.

Expiration

When defining users and access methods, ensure that you set a specific session and token duration whenever possible using the DURATION clause.

DEFINE USER username ON DATABASE PASSWORD 'CHANGE_THIS' DURATION FOR SESSION 5d; DEFINE ACCESS account ON DATABASE TYPE RECORD SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) DURATION FOR SESSION 12h;

Default values provided by SurrealDB are intended to support cases where SurrealDB is used as a traditional backend database, which is why sessions do not expire by default.

If you are building an application where your end users will directly connect with SurrealDB, we strongly encourage setting a session expiration that is as short as possible (typically a few hours) to provide a good experience to your users without compromising security.

Expiring user sessions ensures that a user will not be able to remain authenticated long after their access has been revoked. This cannot be done on demand, as users sessions are not persisted in the database. However, unlike tokens, user sessions are not typically susceptible to be stolen, as they exist only in the context of an established WebSocket connection.

DEFINE USER username ON DATABASE PASSWORD 'CHANGE_THIS' DURATION FOR TOKEN 15m; DEFINE ACCESS account ON DATABASE TYPE RECORD SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) DURATION FOR TOKEN 5s;

Tokens, however, are usually stored in the client, like a web browser, and may be stolen by client-side attacks such as a cross-site scripting vulnerability in your application. For this reason, we strongly recommend reducing token duration from the default one hour to the minimum amount of time that your use case can tolerate. Ideally, a token should only be valid for as long as the client needs in order to use the token to establish a session, which can be as little as a few seconds.

Query Safety

When using SurrealDB as a traditional backend database, your application will usually build SurrealQL queries that may need to contain some untrusted input, such as that provided by the users of your application. To do so, SurrealDB offers bind as a method to query (implemented in other SDKs as the vars argument to query). This should always be used when including untrusted input into queries. Otherwise, SurrealDB will be unable to separate the actual query syntax from the user input, resulting in the well-known SQL injection vulnerabilities. This practice is known as prepared statements or parametrised queries. Binding parameters ensures that untrusted data is passed to SurrealDB as SurrealQL parameters, which are independent from the query syntax, preventing SQL injection attacks.

// Rust SDK // Do this: let name = "john"; // User-controlled input. let mut result = db .query("CREATE person SET name = $name") .bind(("name", name)) .await?; // Do NOT do this: let mut result = db .query(format!("CREATE person SET name = {name}")) .await?;
// JavaScript SDK // Do this: let name = "john"; // User-controlled input. let result = await db.query( "CREATE person SET name = $name;", { name: name } ); // Do NOT do this: let result = await db.query("CREATE person SET name = " + name + ";");

Summary

We’ve covered quite a bit about security, let’s summarise:

  • System user authentication uses DEFINE USER and consists of

    • Three levels of access to limit what users can do to the system: root, namespace or database.
    • Role Based Access Control, which further defines what a user can do, with the three built-in roles: OWNER, EDITOR and VIEWER.
  • Record user authentication uses DEFINE ACCESS and consists of

    • SIGNUP: Defines the logic for when a user signs up as a record user and usually creates a new record in a table.
    • SIGNIN: Defines the logic for when a user signs in as a record user and usually checks credentials against table records.
    • By default, record users have no permissions. They don’t use the Role-Based Access Control (RBAC) system and can only access data if allowed by a PERMISSIONS clause, which is defined on every data resource like tables and fields and defaults to NONE.
    • AUTHENTICATE: Can be used to change the record identifier returned by the SIGNUP and SIGNIN clauses and is a good fit for validating specific conditions that are not expected to change during the lifetime of the session.

Here are some best practices to take away:

  • Use the password hashing functions, such as argon2 so that the passwords from your users remain safe even in the event of a compromise.
  • Employ the principle of least privilege and create users at the lowest level possible and with the minimum role in order to be able to perform their duties inside of SurrealDB
  • Ensure that you set a specific session and token duration whenever possible using the DURATION clause and the shortest duration that is practical for your use case.
  • Use prepared statements by binding parameters to ensure that untrusted data is passed to SurrealDB as SurrealQL parameters, these are independent from the query syntax, preventing SQL injection attacks.

I hope you enjoyed this session. See you in the next one!