How to add user authentication to your MVP using FaunaDB
Tags: jamstack, fauna, auth, abac
Published: 2020-08-04 13:27:20

---
title: How to add user authentication to your MVP using FaunaDB
published: true
description: 
tags: Jamstack, FaunaDB, Auth, ABAC
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/b0kvirhii4dsv0lfuc40.png
---

It is the second article in the series of articles covering [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) with Jamstack. The first article is about [How to launch an MVP with Jamstack, FaunaDB and GraphQL with zero operational costs](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm)

As the title suggests, we will be talking about an important feature that most MVPs need - user authentication. Building your own reliable and secure solution is time-consuming. Using a third-party solution with a free tier is often expensive to scale.

In the [first article](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm) where I share how I build my MVP with Jamstack and [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky), I mentioned that [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) offers secure user authentication and attribute-based access control (ABAC) out of the box.

What makes FaunaDB’s ABAC even more powerful is that changes to access rights are reflected immediately because ABAC is evaluated for every query. This means that access can be granted or revoked without requiring a user to re-login. 

In this article, we will see how easy it is to add user authentication features to a website using [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky). We will also allow authenticated users to bookmark courses. All bookmarks are public by default so anyone can see them. To see FaunaDB’s ABAC in action, we will add a feature to make bookmarks private. Here I'd like to stress that nothing is public in [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky). All data that unauthenticated users will see still needs to be made accessible using either a Role or a Key system. [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) is secure by default!

I assume that you have followed the first article and have the starter project with data coming from [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky). If not, the best is to start [here](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm). Alternatively, checkout the branch with the finished implementation of what was covered in the first article running the following code:
```shell
git clone --single-branch --branch article-1/source-data-from-FaunaDB git@github.com:sandorTuranszky/Gatsby-FaunaDB-GraphQL.git gatsby-fauna-db
```
[FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) provides [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) and [Logout](https://docs.fauna.com/fauna/current/api/fql/functions/logout) built-in functions that can be used to create and invalidate user authentication tokens.

For example, we can pass an email and password to the [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function to get an authentication token or an error, if credentials are invalid.

In the first article we didn't have to create resolvers, as [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) did it for us and we were fine with the logic. However, the login process might vary depending on the requirements, For example, we might want to use a username instead of an email. This is why [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) gives us the flexibility to create our own resolvers.

Let’s take a look at our current schema:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hbrsslnlq2bjscdfxly2.png)

It allows us to create authors with the `name` field only (see 3 - `AuthorInput`). To log in, an author will need to have an email and password. Not a big deal that we did not do it right away, because modifying schema with [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) is very easy. All we need is to upload an updated schema. 

Here it is:
```graphql
type Course {
 title: String!
 description: String
 author: User!
 bookmarks: [Bookmark!] @relation
}

type Bookmark {
 title: String
 private: Boolean!
 user: User!
 course: Course!
}

type User {
 name: String!
 email: String!
 role: Role!
 courses: [Course!] @relation
 bookmarks: [Bookmark!] @relation
}

input CreateUserInput {
 name: String!
 email: String!
 password: String!
 role: Role!
}

input LoginUserInput {
 email: String!
 password: String!
}

input LogoutInput {
 allTokens: Boolean
}

type AuthPayload {
 token: String!
 user: User!
}

type Query {
 allCourses: [Course!]
 allBookmarks: [Bookmark!]
 allUsers(role: Role): [User!]
}

type Mutation {
 createUser(data: CreateUserInput): User! @resolver(name: "create_user")
 loginUser(data: LoginUserInput): AuthPayload! @resolver(name: "login_user")
 logoutUser(data: LogoutInput): Boolean! @resolver(name: "logout_user")
}

enum Role {
 AUTHOR
 DEVELOPER
}
```
Notice that the `Author` type was renamed to `User`. The project requirements have changed and we want to have different types of users depending on their roles. This is why we added the `role` field and the `Role enum`.

We also added the `bookmarks` relation for the “Bookmarks” feature and the `email` and `password` fields to the `User` type for login purposes.

The `allAuthors` query was renamed to a generic `allUsers`.

There is also a relation added between `Courses` and `Bookmarks` types that will allow us to highlight which courses have been bookmarked by an authenticated user.

The `createUser`, `loginUser` and the `logoutUser` mutations are self-explanatory. Notice the [@resolver](https://docs.fauna.com/fauna/current/api/graphql/directives/d_resolver) directive. It allows us to define our custom logic for resolvers. The provided `create_user`, `login_user` and `logout_user` names are how we will name our custom resolver functions. In FaunaDB’s documentation, they are referred to as [User-defined functions (UDF)](https://docs.fauna.com/fauna/current/api/graphql/functions)

### Schema update
We have two options to update the existing schema:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/8yas6l84khvguqpwowz2.png)
Option 1: **UPDATE SCHEMA** creates any missing collections, indexes, and functions and overrides existing ones. All other elements of the existing schema remain the same.

Option 2: **OVERRIDE SCHEMA** removes all database elements, such as collections, indexes and functions and creates new once. This may result in loss of data this is why it’s not suitable for production apps. The GraphQL schema evolution is a much better and safer approach. 

You can read about updating schema in more details [here](https://fauna.com/blog/getting-started-with-graphql-part-4-updating-your-schema)

Since we are at an early stage in our project and changes to our schema are quite significant, we will go with option 2 and override our existing schema. This will require us to repopulate the database with test data and it’s acceptable for us. 

Copy the above schema and paste inside a `schema-courses.gql` file (you can name it whatever you want)

Click the **OVERRIDE SCHEMA** link, choose the file, click **Open** and wait 1 min. As explained [here](https://docs.fauna.com/fauna/current/api/graphql/endpoints#modes), there is a 60-second pause to allow all cluster nodes to process the schema changes. 

Note that fields in the database collections that are no longer declared in the schema are not accessible via GraphQL queries. It might make sense to clean them up before overriding the schema. You can do it with `delete*` mutations in the GraphQL playground.

Before the override, we had two collections populated with some data. 
The `Author` collection:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hz6gl0vd1cfm88b1w65v.png)
And the `Course` collection:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0xr005imrkpzs8b7ws39.png)
After the schema update, we have two newly created `Course` and `User` collections, that are both empty. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/e97p1kwdgcolo6lypzto.png)
As expected, everything was removed and we will need to repopulate our collections with test data, but this time users will have emails, passwords and roles.

### UDF to create a user
Before we can create users, we need to define the `create_user` resolver function and use FaunaDB’s built-in authentication feature to ensure passwords are hashed.

Although the create_user (UDF) has been created by [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) automatically as a "template" UDF based on the [@resolver](https://docs.fauna.com/fauna/current/api/graphql/directives/d_resolver) directive, we will get an error if we try to run the `createUser` mutation now. This is because no logic has been implemented yet.

We will use [Fauna Query Language (FQL)](https://docs.fauna.com/fauna/current/api/fql/) to update the `createUser` UDF. If you prefer, you can create and modify UDF via the dashboard under the **FUNCTIONS** menu.

Here is the FQL code:
```javascript
Update(
  Function("create_user"),
  {
    "body": Query(
      Lambda(["data"],
        Create(
          Collection("User"),
          {
            credentials: { password: Select("password", Var("data")) },
            data: {
              name: Select("name", Var("data")),
              email: Select("email", Var("data")),
              role: Select("role", Var("data")),
            },
          }
        )
      )
     )
  }
)
```
What’s happening in this FQL code.
As mentioned before, the `create_user` UDF has already been created by [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky). It looks like this:
```javascript
Query(
  Lambda(
    "_",
    Abort(
      "Function create_user was not implemented yet. Please access your database and provide an implementation for the create_user function."
    )
  )
)
```
To add custom logic to an existing UDF, we use the [Update](https://docs.fauna.com/fauna/current/api/fql/functions/update) function. It accepts two arguments: `ref` and `param_object`.

As the first argument, we pass in the reference to our existing `create_user` UDF with the help of the [Function](https://docs.fauna.com/fauna/current/api/fql/functions/function) function.

As the second argument, we provide an object with a single key `body`, that holds a query to be run when the function is executed. This query must be wrapped in a [Query](https://docs.fauna.com/fauna/current/api/fql/functions/query) function, which takes a [Lambda](https://docs.fauna.com/fauna/current/api/fql/functions/lambda) function and defers its execution because we want this Lambda function to only run when the mutation is called. 

The Lambda function is used to execute custom code - in our case, the custom code is how we create a new user.

The UDF accepts an array of arguments (`Lambda(["data"],...`), the same arguments as defined in the GraphQL schema. We have an argument `data` in `createUser(data: CreateUserInput)` mutation with values defined in `CreateUserInput`.

The [Create](https://docs.fauna.com/fauna/current/api/fql/functions/create) function is used to create a document in a collection. It takes two arguments: `collection` and `param_object`.

As the first argument, we pass in the references to the `User` collection with the help of the [Collection](https://docs.fauna.com/fauna/current/api/fql/functions/collection) function.

As the second argument, we provide and object with two keys:
- `data` key is an object that holds fields to be stored in the document. In our case, those fields are `name`, `email` and `role`. The `password` will NOT be stored here!
- `credentials` key is an object that encrypts values and this is why we use it to store the password. Once created, this value can’t be retrieved anymore. This means that passwords or other sensitive data can’t be leaked accidentally. 

The [Var](https://docs.fauna.com/fauna/current/api/fql/functions/var) statement returns a value stored in a named variable - in the `data` object in our case and the [Select](https://docs.fauna.com/fauna/current/api/fql/functions/select) function extracts a single value by the key name. For example, the following `Select("name", Var("data"))` is same as `Select("name", {name: "Johns Austin", email: "johns.austin@email.com", password: "password", role: AUTHOR})` that will return the value of the `name` key:  `"Johns Austin"`

Now that it’s clear how exactly our custom `createUser` UDF works, navigate to the Shell (1) page, copy and paste (2) the FQL code and click **Run Query** (3) as shown on the following screenshot:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/585jlkghlrk9bya2e54f.png)
Now, go to the **Functions** page, and you should see the `create_user` function there:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/f26naq0qq8chgo2kp86r.png)

### Create test users
Now we can repopulate our database running two simple FQL queries.
The first one is an index that we will need to set up relations between bookmarks and courses. Copy, paste it into the Shell and run it (similarly like we did above)

```javascript
CreateIndex({
  name: "course_by_title",
  source: Collection("Course"),
  terms: [{ field: ["data", "title"] }],
  values: [{ field: ["data", "title"] }]
})
```
The next script will add all the test data we need. It will create authors with courses, developers and bookmarks. Copy, paste it into the Shell and run it.

```javascript
Map(
  [
    {
      name: "Johns Austin",
      email: "johns.austin@email.com",
      password: "password",
      role: "AUTHOR",
      courses: [
        {
          title: "React for beginners"
        }
      ]
    },
    {
      name: "Andrews Winters",
      email: "andrews.winters@email.com",
      password: "password",
      role: "AUTHOR",
      courses: [
        {
          title: "Advanced React"
        }
      ]
    },
    {
      name: "Wiley Cardenas",
      email: "wiley.cardenas@email.com",
      password: "password",
      role: "AUTHOR",
      courses: [
        {
          title: "NodeJS Tips & Tricks"
        },
        {
          title: "Build your first JAMstack site with FaunaDB"
        },
        {
          title: "VueJS best practices"
        }
      ]
    },
    {
      name: "Blake Fletcher",
      email: "blake.fletcher@email.com",
      password: "password",
      role: "AUTHOR",
      courses: [
        {
          title: "Mastering Vue 3"
        }
      ]
    },
    {
      name: "Hamilton Lowe",
      email: "hamilton.lowe@email.com",
      password: "password",
      role: "DEVELOPER",
      bookmarks: [
        {
          title: "React for beginners",
          private: false
        },
        {
          title: "VueJS best practices",
          private: true
        }
      ]
    },
    {
      name: "Melinda Haynes",
      email: "melinda.haynes@email.com",
      password: "password",
      role: "DEVELOPER",
      bookmarks: [
        {
          title: "Advanced React",
          private: false
        },
        {
          title: "Build your first JAMstack site with FaunaDB",
          private: false
        }
      ]
    }
  ],
  Lambda(
    "data",
    Let(
      {
        userRef: Select(
          "ref",
          Call(Function("create_user"), [
            {
              name: Select("name", Var("data")),
              email: Select("email", Var("data")),
              password: Select("password", Var("data")),
              role: Select("role", Var("data"))
            }
          ])
        ),
        courses: Map(
          Select("courses", Var("data"), []),
          Lambda(
            "course",
            Create(Collection("Course"), {
              data: {
                title: Select("title", Var("course")),
                author: Var("userRef")
              }
            })
          )
        ),
        bookmarks: Map(
          Select("bookmarks", Var("data"), []),
          Lambda(
            "bookmark",
            Create(Collection("Bookmark"), {
              data: {
                title: Select("title", Var("bookmark")),
                private: Select("private", Var("bookmark")),
                user: Var("userRef"),
                course: Select("ref", Get(Match(Index("course_by_title"), Select("title", Var("bookmark")))))
              }
            })
          )
        )
      },
      {
        result: "Success"
      }
    )
  )
)
```
### Rebuilding the app
If you rebuild our project with `gatsby develop`, you will see an error:
```javascript
"There was an error in your GraphQL query: Insufficient privileges to perform the action"
```
Because we replaced the `Author` collection with the new `User` collection, we need to update our `Guest` role privileges that we created in the previous article. We have seen how to manage roles and privileges using the UI - it’s very comfy and easy to understand. In this article, I will use FQL for brevity reasons and will explain what changed.

This is what our `Guest` role privileges look like now. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gjkjd8kyaa69luwibo3v.png)

The `Author` collection and `allAuthors` index do not exist anymore - we need to fix it. 

Run the following FQL query from the Shell to update the `Guest` role privileges.
```javascript
Update(Role("Guest"), {
  privileges: [
    {
      resource: Collection("User"),
      actions: {
        read: true
      }
    },
    {
      resource: Collection("Course"),
      actions: {
        read: true
      }
    },
    {
      resource: Collection("Bookmark"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("allUsers"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("allCourses"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("allBookmarks"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("bookmark_user_by_user"),
      actions: {
        read: true
      }
    }
  ]
})
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1xzvup7uulrsssmh5q9b.png)

Head over to the role management section and select the `Guest` role. You should see this:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d8y74glfyy7z2old2hrm.png)

The `Author` collection is gone and the `Guest` role can read the new `User` collection and `allUsers` index.

Since bookmarks can be public, unauthenticated users can see them. We need to allow the `Guest` role to read the `Bookmark` collection, the `allBookmarks` and the `bookmark_user_by_user` indexes.

If you rebuild our project with `gatsby develop` now, you will see that the list of courses and authors looks exactly as it was before we updated the schema. 

### UDF for user login
To manage the Bookmarks feature, we need to allow users to log in. Let’s create the `login_user` UDF. Here is the code:
```javascript
Update(
  Function("login_user"),
  {
    "body": Query(
      Lambda(
        ["data"],
        Let(
          {
            response: Login(
              Match(Index("user_by_email"), Select("email", Var("data"))),
              { password: Select("password", Var("data")) }
            )
          },
          {
            data: {
              token: Select("secret", Var("response")),
              user: Select("instance", Var("response"))
            }
          }
        )
      )
    )
  }
)
```
It uses FaunaDB’s built-in [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function, which takes two arguments: `identity` and `param_object`.

For the `identity` argument, we pass in a reference to a `User` document which we look up by the provided email. To find a user by an email the `user_by_email` index is used (we will create it soon).

The [Match](https://docs.fauna.com/fauna/current/api/fql/functions/match) function finds the exact match in the given index for the provided search terms. 

As the second argument, we pass in the provided password.

The [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function will return an object containing the `secret` under the `token` key, the reference to the document (user) and some other data.

However, we need to have a custom response structure with the `token` and `user` keys. To restructure the response, we first store the `Login` function response in the `response` variable using the [Let](https://docs.fauna.com/fauna/current/api/fql/functions/let) statement and then shape the response object to match what we need and wrap it in the `data` object because a UDF must return a GraphQL-compatible type.

When authentication fails, the [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function returns an error.

Before we add the `login_user` UDF, we need to create the `user_by_email` index. Click on the **Indexes** menu item and then click **NEW INDEX** link as shown on the next screenshot:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2xgx3p650fn4gokksdt8.png)
Fill in the form as shown in the following screenshot:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2ufynuabdbnffo851e2a.png)
1. Choose the `User` collection
2. Add index name
3. Type `email` in the “Terms” input and click the “+” icon on the right - it will automatically prefix the `data.` part -> `data.email`
4. Check "Unique" checkbox (we need an exact match)
5. Save it

Now we can test the newly created index on the Index page (**Indexes** menu item)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/svf6tg9u1kdjfmhvbfc6.png)
1. Paste the following email `wiley.cardenas@email.com`
2. Click the "Search" button 
3. Get the result

One more thing is left - we need to allow the `Guest` role to read the `user_by_email` index and call the `login_user` UDF.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tnoivem5irsvco59xy7d.png)
1. Navigate to the Security page and click **MANAGE ROLES**, then choose the `Guest` role. 
2. Select the `user_by_email` index from the dropdown
3. Add “Read” action 
4. Select the `login_user` UDF from the dropdown
5. Add “Call” action and save

Now we are ready to update the `login_user` UDF the same way as we added the `create_user` one. Copy the `login_user` UDF code, paste it into the Shell and run the query:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7o95afxzjihvz2v19drt.png)

Head over to the GraphQL Playground to test the `Login` mutation:
Run the following mutation and you will receive the token and the user details:
```javascript
mutation Login {
  loginUser(data: {
    email: "wiley.cardenas@email.com"
    password: "password"
  }) {
    token
    user {
      name
      email
      role
    }
  }
}
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z5s74ng35ncwwu8nqzg3.png)
Try to provide invalid credentials and you will get an error: `authentication failed`

### UDF for user logout
We also need to create a `logout_user` UDF to allow users to invalidate their login sessions. Here is the UDF code:
```javascript
Update(
  Function("logout_user"),
  {
    "body": Query(Lambda(["data"], Logout(Select("allTokens", Var("data"), false))))
  }
)
```
Let’s quickly add it the same way as we did with the `login_user` UDF - via the Shell. See the screenshot:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/aiiflo10ucas3fnrgqsu.png)

### Testing how it works on a real app
The best way to test out the login and logout, as well as the ABAC features, is to see how it works on a real app. I’ve added features to our starter and you can clone the source code from a branch in the same repo. This way, we will save time on copy and pasting a lot of code.

Before you clone, note that you will need the `.env.development` and `.env.production` files from the [first article](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm) containing the bootstrap key and other variables.

Follow these steps:
- `git clone --single-branch --branch article-2/authentication-ABAC git@github.com:sandorTuranszky/Gatsby-FaunaDB-GraphQL.git gatsby-fauna-db`
- `cd gatsby-fauna-db`
- `npm install`
- copy the `.env.development` and `.env.production` files into the root of the project
- `gatsby develop`

You should see our updated app with a few menu options including the Login.

Note if you get any `TypeErrors`, delete the `token` cookie and the `user_data` object from the localStorage.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ynfyytbuvlq55b9c4i3t.png)
The main page contains the same static data as before. If you navigate to the **Developers** page, you will see all public bookmarks being loaded dynamically.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/caefuoz3dds7437g9be8.png)

If you reload this page multiple times, you will see the **Loading...** message for a while before the bookmarks are listed. And if you navigate to the devs tools, you’ll notice the `/graphql` request that is made to load bookmarks dynamically.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4c8vaki1phg5t3b7q84q.png)
This is how easy it is to have a mix of static and dynamic pages with Gatsby.

There is one issue with bookmarks though. If you take a closer look, you will see that one private bookmark is listed together with the public once for unauthenticated users:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/svdvo62jfw5lus87k7dt.png)
This is wrong and we will fix it using FaunaDB’s ABAC.

1. Navigate to the role management page and select the `Guest` role.
2. Add the following FQL code as shown on the screenshot and save it.
```javascript
Lambda(
  "bookmarkRef",
  Let(
    {
      bookmark: Get(Var("bookmarkRef")),
      private: Select(["data", "private"], Var("bookmark"))
    },
    Equals(Var("private"), false)
  )
)
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/077jkeaubvg1qjs91rhu.png)

The FQL code above makes sure that only public bookmarks are accessible for the `Guest` role by checking whether the `private` property on a bookmark is `false`.

Reload the `/developers` page, and you should see no private bookmarks listed. No need to rebuild the app, because bookmarks are loaded dynamically. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2nn9wvg04nryzh265gkd.png)

This is how easy it is to manage what users can access and what not using FaunaDB's ABAC.

### Testing the login feature
**Security notice:** In our example app, we will call a FaunaDB’s GraphQL endpoint right from the client-side. **I strongly discourage you from taking this approach if you will manage sensitive data in your application.**

The right way is to call an endpoint on your server under the same domain which would then communicate with third-party APIs. It will also allow you to implement security techniques and best practices to mitigate common client-side vulnerabilities.

In our example app, we deal with bookmarks which aren't sensitive information at all and this is why we can go with this simplified approach without a backend although it would be easy to implement thanks to Netlify functions. 

**Token invalidation notice:** Tokens created by FaunaDB’s [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function do not have a default Time-To-Live (TTL) value. You can set TTL optionally, however, at the time of the writing of this article, the TTL does not guarantee the token removal. The good news is that a reliable token invalidation is being developed by FaunaDB’s dev team and will become available soon. I will update this article once the feature is released.

It is not an issue for our MVP (users can stay logged-in indefinitely) therefore we will not address token invalidation in this tutorial.

More on authentication-related security you can read [here](https://forums.fauna.com/t/do-i-need-a-backend-api-between-faunadb-and-my-app-what-are-the-use-cases-of-an-api/95) 

To test out how the login/logout works, we need to create a new, `Developer` role and define privileges for it.

Run the following FQL query using the Shell to create the `Developer` role with required privileges.
```javascript
CreateRole({
  name: "Developer",
  membership: [
    {
      resource: Collection("User"),
      predicate: Query(
        Lambda("userRef", 
          Equals(Select(["data", "role"], Get(Var("userRef"))), "DEVELOPER")
        )
      )
    }
  ], 
  privileges: [
    {
      resource: Collection("User"),
      actions: {
        read: true,
      }
    },
    {
      resource: Collection("Bookmark"),
      actions: {
        read: Query(
          Lambda(
            "bookmarkRef",
            Let(
              {
                bookmark: Get(Var("bookmarkRef")),
                userRef: Select(["data", "user"], Var("bookmark")),
                private: Select(["data", "private"], Var("bookmark"))
              },
              Or(
                Equals(Var("userRef"), Identity()),
                Equals(Var("private"), false)
              )
            )
          )
        ),
        write: Query(
          Lambda(
            ["oldData", "newData"],
            And(
              Equals(Identity(), Select(["data", "user"], Var("oldData"))),
              Equals(
                Select(["data", "user"], Var("oldData")),
                Select(["data", "user"], Var("newData"))
              )
            )
          )
        ),
        create: Query(
          Lambda(
            "data",
            Equals(Identity(), Select(["data", "user"], Var("data")))
          )
        ),
        delete: Query(
          Lambda(
            "ref",
            Equals(Identity(), Select(["data", "user"], Get(Var("ref"))))
          )
        )
      }
    },
    {
      resource: Index("allUsers"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("allBookmarks"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("bookmark_user_by_user"),
      actions: {
        read: true
      }
    },
    {
      resource: Function("logout_user"),
      actions: {
        call: true
      }
    },
    {
      resource: Collection("Course"),
      actions: {
        read: true,
      }
    },
    {
      resource: Index("allCourses"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("bookmark_course_by_course"),
      actions: {
        read: true
      }
    },
    {
      resource: Index("course_author_by_user"),
      actions: {
        read: true
      }
    }
  ]
})
```
Let’s inspect the Developer role:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0wo2esnndx52wr0pqjjd.png) 
1. Just like the `Guest` role, `Developer` role needs to read the `User`, `Course` and `Bookmark` collections
2. Privileges to read indexes are needed to query data on collections
3. Logged-in users need to call the `logout_user` function to invalidate the auth token.
4. For authenticated users, ABAC gets more complex. We need to control not only what users can read, but also what resources they can change and in what way.
Our logged-in user can read, create, update and delete bookmarks.

Let’s look at the “Read” action:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wbncakgo7eup6wz049k6.png)
Unlike the `Guest` role, the `Developer` role can see its own private bookmarks. The code above (the predicate function) makes sure that if the bookmark is private, the user defined on the bookmark is the same user that is logged-in. 

We can see it in action. Navigate to the `/app/login` page and log in with the following credentials:
`email`: `hamilton.lowe@email.com`
`password`: `password`

You will be redirected to the following page:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5qz56wlqgxifqfebtny8.png)

Now navigate to the `/developers` page where the logged-in user can still see his own bookmarks including private once. However, for the developer Melinda Haynes only public bookmarks are listed.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nsamjgdb20d2qsw7trku.png)
Now log out and log in with `melinda.haynes@email.com` email and `password` password. Note that password is the same for all users to keep things simple. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qyc389wqfgm50roiv0d0.png)
It turns out, Melinda has no private bookmarks. Navigate to `/developers` page:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wpxf30jeagqieasywj7a.png)
We know that Hamilton has a private bookmark, but Melinda can’t see it. 
You can add bookmarks for Melinda and then log in as Hamilton to see that it will work as expected. Or you can mark all bookmarks as private and they will not be visible for other logged-in or unauthenticated users. 

You can also remove the FQL code that controls the “Read” action for bookmarks to see how things will get messed up.

While we were testing, we logged out users successfully that proves that logged-in users are allowed to call the `logout_user` function.

The “Create” action is also controlled by a predicate function.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ofpd01sq6f2737kdhxw2.png)
The idea here is to control that bookmarks are created by users for themselves only. This is more a business logic rather than a security consideration in our case. We can easily imagine a content manager or an admin creating content for others. 

The “Delete” action is similar by logic to the “Create” action - allowing you to delete only your own bookmarks.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ebp6hvin9b3d6fd5vg4g.png)
The “Write” action is controlled by a predicate function to make sure users can only update their own bookmarks and that the original bookmark owner does not change during an update.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/g3oukf27g71mm4aaqc9b.png)

Now let’s look at the membership. By adding the `User` collection, we state that all users who are members for the `User` collection will be granted the privileges we’ve defined for this role, once they obtain a valid `token` using the [Login](https://docs.fauna.com/fauna/current/api/fql/functions/login) function. 
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/n8avl6lr4yb6pus4lpvd.png)

### Conclusion
As you can see, it is really easy to create user authentication flow using [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) built-in Auth and ABAC features. Although we have barely scratched the surface, it was still sufficient enough to demonstrate how powerful and flexible the [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky) ABAC is. 

I mentioned in the [first article](https://dev.to/sandorturanszky/how-to-launch-an-mvp-with-jamstack-faunadb-and-graphql-with-zero-running-costs-lfm), that ABAC rules can also check for date or time for each transaction offering simple solutions to common problems most web apps have - for example, the need to control free trials periods or manage access to paid content.

With that said, the second part of the challenge is done:
1. Users can authenticate and access data depending on the privileges they have.
2. Bookmarks are loaded dynamically on pages where some of the data is static - a mix of static and dynamic content.
3. We reflect what courses have been bookmarked by the logged-in user on static courses data so they can’t bookmark them twice.

Now we have a half static and half dynamic website powered by [FaunaDB](https://dashboard.fauna.com/accounts/register?utm_source=DevTo&utm_medium=referral&utm_campaign=WritewithFauna_MVPwithJamstack_STuranszky). We still have a lot of room for optimization, and I will cover it in future posts. 

But now, the last thing that is left to do is to explore the [Temporality](https://docs.fauna.com/fauna/current/tutorials/temporality.html) feature and see how it can be applied in real-life examples. I will do so in the next article.

**UPDATE:** Here is the [third](https://dev.to/sandorturanszky/how-to-track-change-to-any-content-with-faunadb-s-temporality-feature-2ng8) article.

The live demo of the app is available [here](https://thirsty-kalam-33eb5f.netlify.app/)