DEFINE ACCESS ... TYPE RECORD
A record access method allows accessing SurrealDB as a record user.
Record users allow SurrealDB to operate as a web database by offering mechanisms to define custom signin and signup logic as well as custom table and field permissions.
Requirements
You must be authenticated as a root, namespace or database user before you can define a record access method.
You must select your namespace and database before you can define a record access method.
Statement syntax
Example usage
Below shows how you can define record access using the DEFINE ACCESS ... TYPE RECORD statement.
With JSON Web Token
Successful authentication with a record access method results in SurrealDB generating a JSON Web Token (JWT or, in the context of SurrealDB, just "token") that can be used until its expiration to authenticate as the record user without the need of providing any additional credentials. These tokens can also be issued by third parties and trusted by SurrealDB in order to allow for the authentication process to take place outside of SurrealDB, while the resulting access claims can be provided to SurrealDB inside of a token that it can trust. This feature is provided by the WITH JWT clause, which behaves similarly to the JWT access method.
Since the origin of the claims in the JWT is verified, those claims can be used within SurrealQL in order to provide table and field authorization through an external authenticator using OpenID Connect, OAuth or simply acting as a trusted issuer of a JWT. This can be done by leveraging table permissions to allow or disallow access depending on the values of the claims in the verified token. For example, these claims can be compared with the records in a table to only return those matching certain criteria.
Bear in mind that table and field permissions only apply to record users, which must use tokens that are verified by a RECORD access method. Access provided by namespace and database tokens defined in a JWT access method is equivalent to access from system users, which is above fine-grained permissions. When application users will be the ones directly authenticating with JWT, defining a RECORD access method WITH JWT is most likely the right choice.
Reference the JWT access method documentation for additional information about how JWT tokens can be used in SurrealDB, including verification through JWKS.
The following example shows how record access with a token can be used to grant authorization either by verifying that the id claim in the token (which is used to populate the $auth reserved parameter) matches the record that is being queried from the user table or if the privileged claim is set to true in the token:
You may also use permissions clauses to perform additional verification on other JWT claims that may be required or recommended by the provider of the token, such as verifying that the iss claim matches a specific principal using $token.iss. However, this kind of logic may be better suited for the AUTHENTICATE clause, which is only executed when the token is validated before an authenticated session is established instead of in every query and for each record.
The token payload should at least include the following claims when used to authenticate as a record user in SurrealDB.
When the id claim is present in the token, the fields of the record matching the identifier specified will be accessible through the $auth reserved parameter. For example, if the value of the id claim is user:73q1bl039y6k8z80v55d, and user records have fields such as “name” or “email”, then $auth.name and $auth.email can be used to access those values for the user:73q1bl039y6k8z80v55d record specifically, without them being present in the JWT.
With Issuer
When explicitly defining a way to verify tokens for record access, it is also possible to customize how these tokens are issued by SurrealDB. This allows specifying the algorithm and the signing key, which otherwise default to the HS512 algorithm with a randomly generated 128-character alphanumeric key. Configuring a record access method to sign tokens with specific signing credentials allows third party services to trust tokens issued by SurrealDB by trusting those signing credentials. In this way, an external service may rely on the signup and signin logic that has been implemented for record users in SurrealDB for its own authentication.
Currently, the algorithm for the issuer and the verifier are required to match. For this reason, the issuer algorithm can be omitted if it has already been defined in the WITH JWT clause. Likewise, an issuer does not need to be explicitly defined in the case where a key to verify JWT using a symmetric algorithm has already been defined in the WITH JWT clause, as the same key will also be used to sign the tokens.
The following is an example of defining a record access method that can issue tokens with an asymmetric key pair:
The issuer is implicitly defined when using a symmetric algorithm in WITH JWT:
With refresh token
Caution
Note
Defining a record access method WITH REFRESH will result in an additional bearer key for the record user being returned after successful authentication with the access method. This bearer key is intended to be used as a "refresh token", which is a concept commonly found in standards such as OAuth 2.0.
Unlike authentication tokens (i.e. JWT), refresh tokens (i.e. bearer keys) feature randomly generated opaque strings that contain no authentication information by themselves, but rather a pointer to an access grant that is stored in the datastore. Also unlike authentication tokens, bearer keys such as refresh tokens can be audited and revoked using the ACCESS statement. Refresh tokens are automatically revoked and replaced by a new refresh token whenever used to obtain an authentication token, reducing the time window for exploiting a compromised refresh token. These additional security guarantees allow refresh tokens to be longer-lived than authentication tokens, which in turn encourages making the original authentication tokens as short-lived as technically possible.
By default, refresh tokens will expire after 30 days. However, their duration can be configured using the DURATION FOR GRANT clause, which will accept any duration. If set to NONE, refresh tokens will never expire. It is strongly recommended to set some expiration for refresh tokens to minimize the potential impact of credential stealing attacks.
Because refresh tokens can be used to indefinitely keep a user authenticated with SurrealDB as long as they are exchanged for a new fresh token before they expire, special care should be taken when storing and applications using them should be suitably protected from attacks.
Like other bearer keys, all refresh tokens are stored in the datastore even after they are expired or revoked. This means that using refresh tokens will have a space cost in addition to the performance cost of retrieving and verifying them against the datastore. Refresh tokens are intended to be used only to obtain a new authentication token after the existing one expires and applications should only use them when necessary, such as after receiving a token expiration error. For certain high volume applications, you may want to regularly purge expired refresh tokens to minimize the space used by inactive refresh tokens.
For more information on how to manage existing refresh tokens, see the ACCESS statement.
Example: Signing in with a refresh token
Define a record access method WITH REFRESH:
Sign up with a new user or sign in with an existing user via the HTTP REST API:
Sign in with the refresh token to obtain a new token and refresh token:
With AUTHENTICATE clause
In the context of DEFINE ACCESS ... TYPE RECORD,the authenticate clause can be used to change the record identifier returned by the SIGNIN and SIGNUP clauses or replace the identifier provided in the token when authenticating WITH JWT.
This clause can also be used to log or stop authentication attempts from record users, as it is always executed across signin, signup and token authentication.
Unlike the PERMISSIONS clause, the AUTHENTICATE clause is executed only at the time of authentication, resulting in increased performance for queries that only need to be validated at that point.
On the other hand, permissions queries are executed in every query and for each record, ensuring that any authorization conditions are verified at the time of the query. The AUTHENTICATE clause is a good fit for validating specific conditions that are not expected to change during the lifetime of the session such as the presence of any required token claims.
Example: External authentication providers
Replacing the record identifier that will be used to establish the session is especially useful in scenarios where the token used to authenticate the session does not contain one. This is common when using an external authentication provider, which may only have knowledge of generic user identifiers such as an email address or UUID.
In the below example, we check if the session may already be tied to an existing user by using the $auth reserved parameter, which contains the record identifier of the authenticated user. If we can select the id field from $auth, it means that the token already contained the id for a record that exists in the database. If that is not the case, we can check if the token contains a different claim that we can rely on to uniquely identifies users. In this case we check for an email address. If the email claim is present in the token, we try to retrieve the user from the user table by their email address. If none of the queries return a record, the AUTHENTICATE clause will fail with a generic error. You can also choose to THROW a custom error as shown in the next example.
Example: Failing authentication
Because the AUTHENTICATE clause is always executed across signin, signup and token authentication, it is in a unique position to centralize logic after user credentials are deemed valid, but before the user is completely authenticated.
Below, we show an example of validating if a user is enabled. If this is not the case, we can THROW an error stating why the user cannot authenticate, stopping the authentication attempt with a custom message. You can also choose to not return anything, which results in a generic authentication error. If the user is enabled, we can RETURN the record identifier, which confirms that authentication is successful and specifies that the record user which will be authenticated in the session is the same that was already authenticated.
Example: Auditing and revoking tokens
In addition to what is shown in the previous example, the AUTHENTICATE clause can also be used to create records and access the claims found in the token itself using the $token reserved parameter. These features can be combined to log authentication attempts and stop authentication from completing if some conditions are met.
Below, we show a proof of concept example that leverages the fact that tokens issued by SurrealDB have the standard jti claim, which contains a randomly generated unique identifier for the token. This value can be used to uniquely identify each token that is issued by SurrealDB for the purposes of auditing and revocation.
In this example, we create a new record in the token table for each token that SurrealDB issues after a successful SIGNIN and SIGNUP. This record is identified by the value in the jti(JWT ID) claim. Every time that a token is used to authenticate, we check if the record in the token table with identifier matching the jti(JWT ID) claim has been revoked and, if so, we fail authentication with a custom message. Otherwise, we log the time that the token was used to successfully authenticate in the audit table and continue authentication without changes.
Using IF NOT EXISTS clause
The IF NOT EXISTS clause can be used to define an access method of type RECORD only if it does not already exist. You should use the IF NOT EXISTS clause when defining an access method in SurrealDB if you want to ensure that the access method is only created if it does not already exist. If the access method already exists, the DEFINE ACCESS statement will return an error.
It's particularly useful when you want to safely attempt to define an access method without manually checking its existence first.
Using OVERWRITE clause
The OVERWRITE clause can be used to define an access method of type RECORD and overwrite an existing one if it already exists. You should use the OVERWRITE clause when you want to modify an existing access method definition. If the access method already exists, the DEFINE ACCESS statement will overwrite the existing access method definition with the new one.