Pending completion
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 using a DEFINE USER statement along with the OWNER role.
System users are assigned a level (root, namespace or database) to set the extent of this role in the system. These users have access to, and can create users in, their own space.
For a root user that means all namespaces and databases, for a namespace user that means one namespace and every database in it, and to round it out a database user gets to do this inside a single database.
In this first 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.
Note that the PASSWORD clause turns into a PASSHASH clause in the actual definition, so the actual password isn't stored anywhere. This next example shows how to manually assign an argon2 passhash that we then use in our user definition along with assigning the editor role.
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.
That's why password hashing functions provided by SurrealDB are used instead. These functions ensure that irreversible cryptographic salted hashes are stored instead of the original passwords, so that the passwords from your users remain safe even in the event of a compromise.
These functions return a different output each time, even if the input password is the same.
That means that attackers can't store tables that hold passwords and their hashed outputs, because they will never be the same. All you can do is use a compare function that returns a boolean to tell you if it is a match or not.
If you looked closely at the output, you might have noticed that it took a certain length of time. That's on purpose - the algorithms for these functions are made to be computationally expensive. Not so much that a user logging in would notice, but enough that a hacker trying every type of password won't be able to do it efficiently.
You can see the difference by running these functions fifty times inside a FOR loop, compared to a regular hashing function like crypto::sha512() that is meant to be efficient as opposed to cryptographically secure. The first loop will take about a second, while the second is almost instantaneous.
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 on 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 - and that's it.
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.
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
In addition to system users, SurrealDB also has another kind of user called a record user.
Record users are saved as a record in a database instead of being created 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: What happens when a user signs up as a record user and usually creates a new record in a table.SIGNIN: What happens when an attempt is made to sign in as a record user. Generally you will check credentials against table records here.AUTHENTICATE: Can be used to change the record identifier returned by theSIGNUPandSIGNINclauses.
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 roles like system users do. Instead, they 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 that 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. Here is a simple example of a user table and its related fields.
The fields result in the following:
An authenticated user can select, update and delete its own user record.
An assertion that the email provided by the user is a valid email address.
Forbidding 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
With the user table set up, we now need to define how to allow users to sign up and sign in. Here is a typical configuration.
Our configuration for record access works like this:
The
SIGNUPlogic needs thename,emailandpasswordparameters to be provided by the user. In the query, we can use them as$name,$emailand$password.The
SIGNINlogic needs theemailandpasswordparameters to be provided by the user. In the query, we can use them as$emailand$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. In our example we validate whether a user is enabled or not. If the user in not enabled, we can use THROW to return a custom error message.
If the statements 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.
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.
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 bindings 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.
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,EDITORandVIEWER.
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. 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
PERMISSIONSclause, which is defined on every data resource like tables and fields and defaults toNONE.AUTHENTICATE: Can be used to change the record identifier returned by theSIGNUPandSIGNINclauses 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
argon2so 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
DURATIONclause 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!