This course requires authentication.
Please sign in to continue
There are multiple forms of authentication built into SurrealDB, supporting different use cases.
In this lesson, we’ll cover:
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.
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.
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.
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 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
.
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!
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:
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:
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
.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.
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.
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.
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 + ";");
We’ve covered quite a bit about security, let’s summarise:
System user authentication uses DEFINE USER
and consists of
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.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:
argon2
so that the passwords from your users remain safe even in the event of a compromise.DURATION
clause and the shortest duration that is practical for your use case.I hope you enjoyed this session. See you in the next one!