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 record users.
- 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 --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: {
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 };
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 permissions and fields in SurrealDB
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;
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 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 check the token claims and map the email address to a record user.
AUTHENTICATE {
-- The issuer claim must match the URL of your AWS Cognito user pool.
IF $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
}
}
WITH JWT URL "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.
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
.
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.