AWS Cognito as the authentication provider for client-side 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:
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 AWS Cognito.
This guide assumes the following:
You have a fresh instance of SurrealDB running with version 1.2.0
or later.
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 have an Amazon Web Services account. By following this guide you will be subject to at least the AWS Cognito and AWS Lambda pricing plans. Although the resources used in this guide should be well within the free tier for both, we cannot guarantee that this will be the case in your particular situation.
In this section, we will create a user pool and a client that will be used to authenticate users with AWS Cognito.
Cognito offers user pools as a user directory for mobile and web applications. This directory can hold users defined directly within Cognito, but can also integrate with third-party identity providers like Google and Facebook. Additionally, it provides the ability to handle user registrations directly in the Cognito Hosted UI, which can also take care of requiring specific information, verifying user email addresses and phone numbers or even enforcing multi-factor authentication. Within a user pool, creating a client establishes a method (in our case a client-side web application) to authenticate with the user pool.
You will need to create a user pool that meets your requirements. In order to be able to follow along with this guide, the following configuration options are strongly recommended:
ImportantThese suggested configuration options may not provide the strongest security or match your specific requirements, but will ensure that you are able to follow with this guide to understand the simplest example of an integration between SurrealDB and AWS Cognito, which you should later update to meet your requirements. Changes in these options may require changes in the other steps outlined in this guide. Other configuration options can be left with their default values or may be changed to fit your requirements.
Once you user pool has been created, open it in the AWS Console and take note of the following values:
Cognito is now ready to perform authentication and issue tokens for your application. However, SurrealDB expects these tokens to contain some specific claims. Cognito allows modifying token claims through pre-token generation lambda triggers. Specifically, we will be adding custom claims to the token.
To create the trigger, just create an AWS Lambda function with the following code:
const handler = async (event) => { event.response = { claimsOverrideDetails: { claimsToAddOrOverride: { ac: "cognito", // The access method that has been defined using DEFINE ACCESS. ns: "test", // The namespace selected when calling DEFINE ACCESS. db: "test", // The database selected when calling DEFINE ACCESS. }, }, }; return event; }; export { handler };
const handler = async (event) => { event.response = { claimsOverrideDetails: { claimsToAddOrOverride: { tk: "cognito", // The name of the token given when defining it with DEFINE TOKEN. sc: "user", // The scope that the token has been defined for with DEFINE TOKEN. ns: "test", // The namespace selected when calling DEFINE TOKEN. db: "test", // The database selected when calling DEFINE TOKEN. }, }, }; return event; }; export { handler };
Note that, in order to use the suggested code, the function must be configured as “Node.js 20.x” or equivalent.
After the Lambda trigger has been created, we will need to associate it with our client integration. Visit your user pool in Cognito and, under the “User pool properties” tab, in the “Lambda triggers” section, click on “Add Lambda trigger”. For this guide, the trigger type will be “Authentication”, specifically a “Pre token generation trigger”. Select the function that you created.
Depending on how you configured your user pool, users will be able to register by different means. To ensure this guide can be followed easily, we will create a test user that to authenticate in our web application. Open your user pool and click on the “Create user” button under the “Users” section in the main user pool view. Provide an email address for that user and check “Mark email address as verified” to avoid having to do so manaually. Set a password that matches the requirements that you configured.
For this simple example, we will create a single table named “user”, where any user that authenticates through AWS Cognito 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 = "cognito" -- 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 cognito_username 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 AWS for your Cognito user pool. This JWKS object can be found in an endpoint build from your AWS region and user pool in the format https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json
. Pointing to a remote JWKS object ensures that token verification will work seamlessly even in the case that AWS rotates their encryption 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 AWS Cognito. This is required because AWS Cognito 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 AWS Cognito in order to retrieve the corresponding record user.
The following queries will create the required resources to authenticate a token for a record using JWKS:
-- 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 your AWS Cognito user pool. -- The name of the access method should match the custom claim that we configured before. DEFINE ACCESS cognito ON DATABASE TYPE RECORD -- We verify the token using the public keys hosted by AWS. WITH JWT URL "https://cognito-idp.<YOUR_AWS_REGION>.amazonaws.com/<YOUR_COGNITO_USER_POOL_ID>/.well-known/jwks.json" -- We check the token claims and map the email address to a record user. AUTHENTICATE { IF ( -- The issuer claim must match the URL of your AWS Cognito user pool. $token.iss = "https://cognito-idp.<YOUR_AWS_REGION>.amazonaws.com/<YOUR_COGNITO_USER_POOL_ID>" -- The audience claim must match you AWS Cognito Client ID. AND $token.aud = "<YOUR_COGNITO_CLIENT_ID>" -- The email address in the token must be verified as belonging to the user. AND $token.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.email } } ;
In the example above, replace the placeholders with values applicable to your Cognito user pool.
It is important to know that validating the issuer and audience of the token is a requirement of AWS Cognito, other providers may require validating additional claims to ensure that the token is being used as intended.
In order to allow SurrealDB to establish a connection with AWS Cognito to download the JWKS object, you will require running it with the network capability.
For the strongest security, provide your specific Cognito user pool domain when starting SurrealDB with --allow-net
. For example: --allow-net cognito-idp.eu-west-1.amazonaws.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 AWS for your Cognito user pool. This JWKS object can be found in an endpoint build from your AWS region and user pool in the format https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json
.
Pointing to a remote JWKS object ensures that token verification will work seamlessly even in the case that AWS rotates their encryption 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 using JWKS:
-- 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 your AWS Cognito user pool. -- The name of the token should match the custom claim that we configured before. DEFINE TOKEN cognito ON SCOPE user TYPE JWKS VALUE "https://cognito-idp.<YOUR_AWS_REGION>.amazonaws.com/<YOUR_COGNITO_USER_POOL_ID>/.well-known/jwks.json"; ;
In the example above, replace the placeholders with values applicable to your Cognito user pool.
In order to allow SurrealDB to establish a connection with AWS Cognito to download the JWKS object, you will require running it with the network capability.
For the strongest security, provide your specific Cognito user pool domain when starting SurrealDB with --allow-net
. For example: --allow-net cognito-idp.eu-west-1.amazonaws.com
.
For this example, we will create a single table named user
, where any user that authenticates through AWS Cognito 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 issuer claim must match the URL of your AWS Cognito user pool. AND $token.iss = "https://cognito-idp.<YOUR_AWS_REGION>.amazonaws.com/<YOUR_COGNITO_USER_POOL_ID>" -- The audience claim must match you AWS Cognito Client ID. AND $token.aud = "<YOUR_COGNITO_CLIENT_ID>" -- The email claim must match the email of the user being queried. AND email = $token.email -- The email must be verified as belonging to the user. AND $token.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 cognito_username ON user TYPE string;
It is important to know that validating the issuer and audience of the token is a requirement of AWS Cognito, other providers may require validating additional claims to ensure that the token is being used as intended.
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.
For this guide, we will create a simple client-side web application that will direct the user to log in using the Cognito Hosted UI, redirect the user back your our application with a query parameter containing an authorization code and exchange that code to Cognito for the user identity token which we will use to authenticate with SurrealDB. The application will create or update a SurrealDB user using data from the token claims. This user will later be able visit our web application and retrieve their information from SurrealDB.
NoteAs most other authentication providers using OpenID Connect (OIDC), AWS Cognito issues both identity and access tokens. In this example, we will be using the identity token as it can include custom claims (which are required by SurrealDB) by default. It is important to note that identity tokens should only be used to assert identity claims as opposed to access claims. In this case, we trust the identity token to provide information about the indentity of the user. If we wanted the token to contain authorization claims (e.g. OAuth scopes) we should instead rely on the access token. Customizing access token claims has been recently supported by AWS and requires enabling advanced security features. This guide makes the deliberate decision of using the identity token to simplify the process.
We have developed an example application that uses plain JavaScript to authenticate with Cognito using basic HTTP requests against the login endpoint of the Cognito Hosted UI and the Cognito token endpoint. For the purposes of following this guide, we recommend using our example code. However, keep in mind that this code aims to be as simple as possible and is not suitable for production applications. Alternatively, you can develop this application yourself using the Cognito SDK or the new Amplify SDK.
When using our example code, you will only need to update the config.json
file with the values that you saved after creating your user pool and run the web application using the start.sh
script or any equivalent web server. Once in the web application, clicking the “Log in” button should take you to the Cognito Hosted UI to log in with your test user, after which you should be redirected back to your web application. For this to work, the URL of your web application (without a trailing slash) should be present in the “Allowed callback URLs” list that you defined when configuring the user pool client. After redirection, the web application will show some information about the authenticated user and attempt to create it in SurrealDB via the configured endpoint. After the user is created, subsequent logins will retrieve its information from SurrealDB and display it in the web application. If that is not the case, use the developer console of your browser together with the SurrealDB logs (running with --log trace
during the debugging for maximum verbosity) to understand why.
Once the example web application is working, you can inspect the simple code under app.js
to understand how.
In this section, we will provide a few examples of how to configure the application to work with AWS Cognito.
You can view and download a minimal example of an web application using AWS Cognito and SurrealDB in the AWS cognito example project.