Integrate AWS Cognito as an Authentication Provider
This guide will cover using AWS Cognito as the authentication provider for client-side web applications using SurrealDB as the only backend. In this guide you will learn how to:
- Configure AWS Cognito to issue tokens that can be used with SurrealDB.
- Configure SurrealDB to accept tokens issued by AWS Cognito.
- Define user-level authorization using SurrealDB scopes.
- Authenticate users with AWS Cognito in a client-side web application.
- Retrieve and update information from SurrealDB using the authenticated user.
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.
Prerequities
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 --log trace --auth --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.
Configuring AWS Cognito
Creating a User Pool and a Client
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:
- In the "Configure sign-in experience" step:
- Select at least "Email" in "Cognito user pool sign-in options".
- In the "Configure sign-up experience" step:
- Select at least "email" in "Required attributes".
- Select "Allow Cognito to automatically send messages to verify and confirm".
- In the "Configure message delivery" step:
- Select "Send email with Cognito" in "Email".
- In the "Integrate your app" step:
- Select "Use the Cognito Hosted UI" in "Hosted authentication pages".
- Select "Use a Cognito domain" in "Domain" and set a name for it.
- Select "Public client" in "Initial app client".
- Select "Don't generate a client secret" in "Initial app client".
- Under "Allowed callback URLs" in "Initial app client", set the URL of your web application.
- Under "Advanced app client settings" in "Initial app client", ensure "Authorization code grant" is selected.
- Under "Attribute read and write permissions" in "Initial app client", ensure "email" is selected for "Read".
- Under "Attribute read and write permissions" in "Initial app client", ensure "email_verified" is selected for "Read".
Note: These 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:
- "User pool ID" from the "User pool overview" section in the main user pool view.
- "Cognito domain" from the "Configuration for all app clients" section under the "App integration" tab.
- "Client ID" from the only client in the "App client list" under the "App integration" tab.
Creating a Pre-Token Generation Lambda
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: {
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.
Creating a Test User
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.
Configuring SurrealDB
Defining a token verification method in SurrealDB
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
.
Defining authorization criteria in SurrealDB
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.
Creating a Simple Web Application
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.
Note: As 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 (ideally running with --log trace
) to understand why.
Once the example web application is working, you can inspect the simple code under app.js
to understand how.
Annex
Example Single Page Application
You can view and download a minimal example of an web application using AWS Cognito and SurrealDB in the AWS cognito example project.