🚀 Fauna Architectural Overview White Paper: Learn how Fauna's database engine scales with zero ops required
Download free ->
Fauna logo
FeaturesPricing
Learn
Customers
Company
Support
Log InContact usStart for free
Fauna logo
FeaturesPricing
Customers
Start for free
© 2024 Fauna, Inc. All Rights Reserved.
<- Back
fauna_cognito

Authenticating users with AWS Cognito in Fauna

Shadid Haque|Sep 17th, 2021|

Categories:

AuthenticationTutorialAmazon Web Services
⚠️ Disclaimer ⚠️

This post refers to a previous version of FQL.

This post refers to a previous version of FQL (v4). For the most current version of the language, visit our FQL documentation.

One of the main components of any modern application is user authentication. AWS Cognito is a popular auth provider from Amazon Web Services. It lets you add user sign-up, sign-in, and access control to your web and mobile apps. In this article, you learn how to use AWS Cognito to authenticate to Fauna, the data API for modern applications.

Get the complete code for this article in the following link

Github - Authenticate with Cognito

✔️ Pre-requisites

This article assumes that you have some basic understanding of AWS Cognito and Fauna Authentication. If either of these concepts is completely new to you, review the following tutorials before proceeding.Using Amazon Cognito Identity to authenticate usersUser Management with AWS CognitoFauna | User authentication

AWS Cognito and Fauna authentication architecture

AWS Cognito is a closed system that does not allow a JWT (JSON Web Token) handshake with external systems like Fauna. This prevents you from adding Cognito as an external provider in Fauna directly. However, Cognito follows an event-driven architecture pattern, providing you with specific triggers that you can execute in certain stages of authentication. You can use the Pre Token Generation trigger to wire up Cognito with Fauna’s native authentication.

ℹ️ Review all the available triggers by logging into your AWS console and navigating to Cognito > User Pools > Name of your User pool > Triggers.

Authentication flow

Review the following diagram of the overall authentication flow that demonstrates how a user authenticates with AWS Cognito and receives an auth token from Fauna.

//images.contentful.com/po4qc9xpmpuh/7qzMEN1wu1bOCEkuH5OJyt/ae30173e88af82f59bb56de81de66a29/pic1.png

The authentication events happen in the following order.

  1. The client application (React app) sends a request to Cognito.
  2. Cognito invokes an AWS Lambda function in the token generation phase.
  3. The Lambda function generates a user-based access_token and refresh_token for Fauna. These tokens auto-expire after a specified time.
  4. Cognito returns all the tokens to the client application.
  5. The client application uses the tokens to communicate with Fauna database.

Creating a new Cognito App

Create a new Cognito User Pool from AWS Console. You must provide a name for your Cognito User Pool. You can choose to step through all the settings of Cognito or accept the defaults.

//images.contentful.com/po4qc9xpmpuh/2ICE51DJ61pum25PgrIU2s/80eaa578b1e9028148f0f8a970e7b64f/pic2.png

Once the User Pool is created, take note of the Pool Id; you need this to connect your client application with Cognito.

//images.contentful.com/po4qc9xpmpuh/6ZALARNY8Eg018D11crtgu/cfb3a2b4538e3907af4a5f8b430660d8/pic3.png

Select App client from the menu and create a new app client. Define your token expirations based on your application requirements and uncheck generate client secret.

//images.contentful.com/po4qc9xpmpuh/4KznseQM8Va98ZAAawDEJo/71e639f18155b0fbaf7ee10bd3c5f09a/Screen_Shot_2021-09-17_at_10.57.58_AM.png

The client secret is an optional parameter that provides an extra layer of security. For simplicity, this tutorial is not utilizing this feature. Review the AWS Cognito documentation for more information.

Once the app client is created, take a note of the App client id. You need this App client id to configure your client application with Cognito.

//images.contentful.com/po4qc9xpmpuh/3gDt1DkGvcXqf8rL2XtILH/4b6f7ca55bb362710dcb3a7d92070b35/pic4.png

Adding Cognito to the client application

Clone the Cognito-Fauna-React starter sample from git by running the following command.

git clone https://github.com/fauna-labs/fauna-cognito-auth

The template provides basic functionality including forms, routing, and cookie storage so that you don’t have to set up everything from scratch.

Open src/config.js. Fill in the UserPoolId with your Cognito Pool id and ClientId with your App client id that you generated in the previous steps.

export const config = {
    UserPoolId: 'us-east-1_teqKLEjjjc',
    ClientId: '7spl4e1pnj5h97jjh8l6b6a5jn'
}

Install the dependencies and start the application by running the following commands.

npm install
npm start

Visit your application at http://localhost:3000. At this point, you should be able to sign up, confirm your account, and login with the sample application.

Fauna configuration

The next step is to configure Fauna. Open the Fauna dashboard and create a new database. Be sure to choose the Classic Region Group.

//images.contentful.com/po4qc9xpmpuh/s5vjSAUy4seGvmgtM6TWi/8e22f3c1a52a0634ecc0e4360854a38c/pic5.png
//images.contentful.com/po4qc9xpmpuh/4h2C7WOnI7x8RGGqjImFq6/93a4da285d62f8ea49f0a8be90673049/pic6.png

Next, navigate to the Collections tab in the dashboard and create a new collection called Account. This collection holds the user information for your application.

//images.contentful.com/po4qc9xpmpuh/0N0WeFPIqKEpK2XztenUz/f24aeb05b4dfcd9819e2089f495afbbe/Screen_Shot_2021-09-17_at_11.22.59_AM.png

You can also create a collection from the Fauna shell by running the following command.

// Create accounts Collection
CreateCollection({ name: 'Account' })
//images.contentful.com/po4qc9xpmpuh/5Kxd18M3JgHxny6CueIv7v/9a299379bb89110c53fda47909916a0b/pic7.png

Navigate to the Indexes tab in your Fauna dashboard and choose New Index. This creates a new index that allows AWS Cognito to query users from your Fauna database by email.

Name your index accounts_by_email. Make sure to define Accounts as the source collection and email as a term. Also, check the unique and serialized checkboxes. This ensures that only one user is allowed per email address.

//images.contentful.com/po4qc9xpmpuh/2nBKao7wWMTRMuf7Y73yUn/ade904aea8dd3bbaf99f185762ef9cbb/pic8.png

You can also create an index by running the following command in the shell.

CreateIndex({
  name: 'accounts_by_email',
  source: Collection('Account'),
  // We will search on email
  terms: [
    {
      field: ['data', 'email']
    }
  ],
  unique: true,
  serialized: true
})

Create another collection called Movie. Only authenticated users will have permission to perform CRUD (Create, Read, Update, Delete) operations on the Movie collection. Populate the Movie collection with some sample data. Select Collection > Movie > NEW DOCUMENT and insert the following test data.

//images.contentful.com/po4qc9xpmpuh/2uHiKEJpyZxwgjpKVpCgiy/bb71ad86dee88d89dda9bbcf4afbcf8b/Screen_Shot_2021-09-17_at_11.39.40_AM.png
{
    "title": "The Hateful Eight",
    "director": "Quentin Tarantino",
    "release": "December 25, 2015"
}
{
    "title": "Once Upon a Time in Hollywood",
    "director": "Quentin Tarantino",
    "release": "July 26, 2019"
}

You must create an access role to allow only logged-in users to perform CRUD on the Movie collection.

To create a new role, select Security > Roles > New Role in the dashboard. Give your new role a name and specify the appropriate permissions for each collection.

//images.contentful.com/po4qc9xpmpuh/5ImuWVtGeTTpU8pNd4SNrq/3864a1d4fa17c5b4b3e10eff1cf93f17/Screen_Shot_2021-09-17_at_11.42.26_AM.png
//images.contentful.com/po4qc9xpmpuh/7HzCFmkhRWhqgOXQgULsLA/7833fa89549a7f713d2a97865718891a/pic9.png

Provide Read Write Create and Delete permission to the Movies collection. Switch to the membership tab and add the Accounts collection as a member.

//images.contentful.com/po4qc9xpmpuh/3k7UFlyuM1l1Rc12mBfsnW/25d0c7a17e5b5a1749453d4b05c6a911/Screen_Shot_2021-09-17_at_12.06.06_PM.png

Provide Read Write Create and Delete permission to the Movies collection. Switch to the membership tab and add the Accounts collection as a member.

Choose Save to create your new role. Because your role is dynamic, it implements attribute-based access control (ABAC). Every record in the Accounts collection is able to generate a token with the necessary privileges to query the collections defined in the Privilege tab.

Finally, create a new Fauna server key from the Security tab. You will need this key for AWS Lambda to communicate with Fauna.

//images.contentful.com/po4qc9xpmpuh/1OrSXCph7rlV4LvhM7FxS3/a741c6e97bffe33d87817087593dca2f/pic11.png

Creating a Lambda function

In the starter code, we included a sample Lambda function to get started. Let’s explore the code together.

Examine aws/lambdas/faunaAuthToken/users.js. The functions in this file are self-explanatory. These helper functions are used by the Lambda service. For instance, the registerUser function inserts new users into the database.

...
// Registers a new User in FaunaDB
const registerUser = (email) => {
    return serverClient.query(
        q.Create(
          q.Collection('accounts'),
          { data: { email } },
        )
    );
};

The getUserByEmail function retrieves a particular user by email.

// Get a user
const getUserByEmail = (email) => {
    return serverClient.query(
        q.Get(
          q.Match(q.Index('accounts_by_email'), email)
        )
    );
};

The createRefreshToken function creates a new refresh token using a user ref. The createAccessToken function is responsible for creating an access token.

const createRefreshToken = (ref, ttl) => {
    return serverClient.query(
        q.Create(q.Tokens(), {
            instance: q.Ref(q.Collection("accounts"), ref),
            data: {
              type: "refresh"
            },
            ttl: q.TimeAdd(q.Now(), ttl, "seconds"),
        })
    );
};

const createAccessToken = (accountRef, refreshTokenRef, ttl) => {
    return serverClient.query(
        q.Create(q.Tokens(), {
            instance: q.Ref(q.Collection("accounts"), accountRef),
            data: {
              type: 'access',
              refresh:  refreshTokenRef
            },
            ttl: q.TimeAdd(q.Now(), ttl, 'seconds')
        })
    );
};

💡 On line 4, you have to define the Fauna server key secret (Generated in the previous step). Please be advised that hardcoding this value is not recommended. You must use a secret manager to maintain this key.💡 Please refer to the following article to learn more about how to manage secrets inside your Lambda functions.

const faunadb = require('faunadb');
const q = faunadb.query;

const serverClient = new faunadb.Client({ secret: '<Your Fauna Server Secrect>' });
// In production do not hardcode this value.
...

Next, take a look at the aws/lambdas/faunaAuthToken/index.js file. This is the main file that runs during the Lambda function invocation from Cognito. The Lambda function has access to the Cognito event. The following code snippet retrieves the email from the Cognito event used in login.

const email = event.request.userAttributes['email'];

Using this email, you can retrieve the user from Fauna. If the user doesn’t exist, this is the first time this user is logging in. In that case, you can create a new user entry in the Fauna database.

try {
    const foundUser = await getUserByEmail(email);
    userId = foundUser.ref.id;

} catch (e) {
    console.log('Error: %s', e);
    const newUser = await registerUser(email);
    userId = newUser.ref.id;
}

Using the user reference, you can generate an access token and a refresh token for Fauna. The following code snippet demonstrates this.

try {
    const faunaRefreshToken = await createRefreshToken(userId, 3600);
    fauna_refresh_token = faunaRefreshToken.secret;
    console.log('fauna_refresh_token', fauna_refresh_token);
    const rret = await createAccessToken(userId, fauna_refresh_token, 600);
    fauna_access_token = rret.secret;
    console.log('fauna_access_token', rret.secret);
} catch (e) {
  console.log('Token Generation Error: %s', e);
}

Finally, you can attach the tokens to the Cognito response object and send the response back to the client application.

event.response = {
    "claimsOverrideDetails": {
        "claimsToAddOrOverride": {
            fauna_access_token,
            fauna_refresh_token,
        },
        "claimsToSuppress": ["email"]
    }
};
context.done(null, event);
return event;

Once you create and upload the Lambda function you must attach it to the Pre Token Generation trigger in AWS Cognito. Go to your User Pool in AWS Console and navigate to Triggers.

//images.contentful.com/po4qc9xpmpuh/2zLqMo96aen7eHsas4wOXT/922e4728c49450ec33c13a4647ef3c67/pic12.png

Attach your Lambda function to the Pre Token Generation trigger and save changes.

//images.contentful.com/po4qc9xpmpuh/6RXcpPPm62RMYSJbPLFkri/0e660b0ee230ccf3ffabdac1808dca41/13.png

Test the client application

Run the client application with the following command.

npm start

Register and confirm a new user.

//images.contentful.com/po4qc9xpmpuh/35oAUz6oviSVdg8TrZbauy/bbdb5ad4669d338a7c25dea2668d2e69/pic14.png
//images.contentful.com/po4qc9xpmpuh/4sheBHn6KXcYxFeL4Nghsh/8b47c4b76c845951dac172c4edc30add/pic15.png

Now, try logging in with your newly registered user. Open up your dev tools in the browser, and you will notice that a Fauna Access token has been generated along with standard Cognito information.

//images.contentful.com/po4qc9xpmpuh/4aGb1mXmUVUQ8A9TPA4alL/23716f65349e87910e453966e39f31b1/pic16.png

Take a look at the src/components/Login.js file. On form submission, the onSubmit function is executed.

const onSubmit = async e => {
      e.preventDefault();
      console.log('State', state);

      const cognitoUser = new CognitoUser({
        Username: state.username,
        Pool: userPool,
      });
      const authenticationDetails = new AuthenticationDetails({
          Username: state.username,
          Password: state.password,
      });

      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: data => {
          setCookie('fauna_access_token', data.getIdToken().payload.fauna_access_token)
          setCookie('cognito_refresh', data.getRefreshToken().getToken())
          setCookie('cognito_username', data.getAccessToken().payload.username)
          console.log('Auth Data', data);
          alert('User Login Successful');
          history.push('/movies');
        },

        onFailure: err => {
          console.log('Failed', err)
        },

        newPasswordRequired: newPass => {
          console.log('New Pass Required', newPass)
        }
      })
  }

On button click, authenticateUser function from AWS identity library is executed. On successful authentication, the fauna_access_token is retrieved from the IdToken and saved into cookies. You can now use this token to query Fauna.

Head over to http://localhost:3000/movies in your browser and click the “Get Movies” button. You should be able to see the queried data in the console.

//images.contentful.com/po4qc9xpmpuh/7oSaPN37fm7LNz6uXCKR6u/3442cbb831725f52b3a26e37d047fca9/pic17.png

The following code demonstrates how the data is being queried using the fauna_access_token.

const queryMovies = async () => {
      const serverClient = new faunadb.Client({ secret: cookies.fauna_access_token });
      try {
          const movies = await serverClient.query(
            q.Map(
              q.Paginate(q.Documents(q.Collection("movies"))),
              q.Lambda("X", q.Get(q.Var("X")))
            )
          )
          console.log('Movies', movies);
        } catch (error) {
          console.log('error', error);
          if(error.name === "Unauthorized") {
            // Refresh Tokens
            alert('Token Expired! Refresh Token or Log in again')
          }
      }
  }

Take look at the src/components/Movies.js file to check out the entire example. The file also contains a refresh token example.

import { CognitoUserPool, CognitoUser, CognitoRefreshToken } from 'amazon-cognito-identity-js';
...
const refreshTokens = () => {
    const cognitoUser = new CognitoUser({
        Username: cookies.cognito_username,
        Pool: userPool,
    });

    const cognito_refresh_token = new CognitoRefreshToken({ RefreshToken: cookies.cognito_refresh })

    cognitoUser.refreshSession(cognito_refresh_token, (err, session) => {
        if (err) {
            console.error(err);
        }
        setCookie('fauna_access_token', session.getIdToken().payload.fauna_access_token);
        alert('Token Refreshed')
    });
}

When you call the CognitoRefreshToken function, it reruns the Cognito Lambda trigger and refreshes the Fauna access token.

Conclusion

Follow this GitHub link to find the complete code for this article. In this article, you learned how to integrate Fauna and Cognito authentication. If you are interested in learning more about Fauna authentication and various strategies to authenticate Fauna then check out the fauna-blueprints repository in Fauna Labs on GitHub.

If you enjoyed our blog, and want to work on systems and challenges related to globally distributed systems, and serverless databases, Fauna is hiring

Share this post

TwitterLinkedIn

Subscribe to Fauna's newsletter

Get latest blog posts, development tips & tricks, and latest learning material delivered right to your inbox.

<- Back