User authentication

This tutorial assumes that you have completed the Quick Start with FaunaDB tutorial.

FaunaDB offers built-in identity, authentication, and password management. This tutorial walks you through how to create user identities, authenticate them, and manage their sessions.

This tutorial is divided into several sections:

Setup

This setup section describes all of the preparatory work we need to do to prepare for authenticating our users. It includes:

Create a database

When we want to authenticate users, it is typically in the context of a specific application. With that in mind, let’s create an application-specific database called "app1". Copy the following query, paste it into the Shell, and run it:

CreateDatabase({
  name: "app1"
})

When you run this query, the result should be similar to:

{ ref: Database("app1"), ts: 1576008456740000, name: 'app1' }

Create a server key

Our application is going to need access to our new database. We don’t want to give it permission to every database, so let’s create a "server" key, which provides full access to a specific database. Copy the following query, paste it into the Shell, and run it:

CreateKey({
  name: "Server key for app1",
  database: Database("app1"),
  role: "server",
})

When you run this query, the result should be similar to:

{ ref: Ref(Keys(), "251405600267698688"),
  ts: 1576017914170000,
  name: 'Server key for app1',
  database: Database("app1"),
  role: 'server',
  secret: 'fnADfSwPQoACAFAfWX9f6NFrBumWqIMsL8Qkt3wY',
  hashed_secret:
   '$2a$05$pC8bzQqEw3EM4TbsPJ6tjOzqnTV2rRZEQhT7sxI1SsdkCfk14n9qq' }
When you run the query, you will see different values. Make sure that you copy the value for the secret field; it is a key that authorizes access to FaunaDB, specifically to the associated database. It is only ever displayed once. If you lose it, a new key would have to be generated.

Create a client key

We need to allow our application’s public clients, typically a web browser, to access our "app1" database, and we need to embed a key into the public client to permit that access. So, let’s create a "client" key. Copy the following query, paste it into the Shell, and run it:

CreateKey({
  name: "Client key for app1",
  database: Database("app1"),
  role: "client",
})

When you run this query, the result should be similar to:

{ ref: Ref(Keys(), "251406008027447808"),
  ts: 1576018302900000,
  name: 'Client key for app1',
  database: Database("app1"),
  role: 'client',
  secret: 'fnADfSxuQuAQAe6JBbRY58Fm37ZA-0HhFtVm64T0',
  hashed_secret:
   '$2a$05$qKd8N/LsdLQ9kQKGmtYa/OjgNCQNlzG5sNO9xT1jWSiBuPMNGREJW' }
When you run the query, you will see different values. Make sure that you copy the value for the secret field; it is a key that authorizes access to FaunaDB, specifically to the associated database. It is only ever displayed once. If you lose it, a new key would have to be generated.

Create a collection to store user documents

Now that we have our app-specific database, and keys to access it, now we can create a collection where we can store user documents.

Let’s use our server key to access the new database. First, type .exit into the Shell and press Return.

Then start the shell using the secret for the server key:

fauna shell --secret=fnADfSwPQoACAFAfWX9f6NFrBumWqIMsL8Qkt3wY
Be sure to replace fnADfSwPQoACAFAfWX9f6NFrBumWqIMsL8Qkt3wY with the secret that you acquired for the server key.

Now, let’s create the collection to store users. Copy the following query, paste it into the Shell, and run it:

CreateCollection({ name: "users" })

When you run this query, the result should be similar to:

{ ref: Collection("users"),
  ts: 1576019188350000,
  history_days: 30,
  name: 'users' }

Create a public index for our users

We need an index to make it possible to lookup our users by their email address. We need this index to be public, since unauthenticated users would be using the client key when they attempt to login. So, let’s create the index. Copy the following query, paste it into the Shell, and run it:

CreateIndex({
  name: "users_by_email",
  permissions: { read: "public"},
  source: Collection("users"),
  terms: [{field: ["data", "email"]}],
  unique: true,
})

When you run this query, the result should be similar to:

{ ref: Index("users_by_email"),
  ts: 1576019648660000,
  active: false,
  serialized: true,
  name: 'users_by_email',
  permissions: { read: 'public' },
  source: Collection("users"),
  terms: [ { field: [ 'data', 'email' ] } ],
  unique: true,
  partitions: 1 }

At this point, the setup is complete!

Create users

When a new user signs up, we can create a new user document that contains their email address and password. More specifically, a hash of the user’s password is stored; FaunaDB does not store credentials in plain text.

Let’s create our first user. Copy the following query, paste it into the Shell, and run it:

Create(
  Collection("users"),
  {
    credentials: { password: "secret password" },
    data: {
      email: "alice@site.example",
    },
  }
)

When you run this query, the result should be similar to:

{ ref: Ref(Collection("users"), "251407645221585408"),
  ts: 1576019864330000,
  data: { email: 'alice@site.example' } }

User login

When a user wants to login, they would provide their email address and password. Then we use the Login function to authenticate their access, and if valid, provide them with a token that they can use to access resources.

A token only provides access according to the privileges granted by an attribute-based access control (ABAC) role. These differ from keys, which are used to provide coarser, database-level access.

The following query calls Login on the result of looking up the user via the users_by_email index, with the password that they provided. Copy the query, paste it into the Shell, and run it:

Login(
  Match(Index("users_by_email"), "alice@site.example"),
  { password: "secret password" },
)

When you run this query, the result should be similar to:

{ ref: Ref(Tokens(), "251407817091580416"),
  ts: 1576020028130000,
  instance: Ref(Collection("users"), "251407645221585408"),
  secret: 'fnEDfS4T34ACAAN9IwrU8aQA5SxTgyqYaUfiAqLqzQjQH9Qcr94' }
When you run the query, you will see different values. Make sure that you copy the value for the secret field; it is a token that authorizes access to FaunaDB, specifically to the associated database. It is only ever displayed once. If you lose it, a new token would have to be generated.

If the user cannot be found, or if their credentials are invalid, an error would be returned:

Login(
  Match(Index("users_by_email"), "bob@not.a.member"),
  { password: "secret password" },
)
[ { position: [],
    code: 'authentication failed',
    description:
     'The document was not found or provided password was incorrect.' } ]

The token provided for a successful login is all that is required to perform authenticated queries; it represents both the identity and authorization for the user. The token can now be used in any subsequent queries for resources.

Your app should use the value in the secret field to create another client instance, which should be used to perform queries as that user.

If your application is using HTTP requests to interact with FaunaDB, you can use the token as a username+password via the Basic-Auth header, for every query made by that specific user. For example, you could use curl:

curl https://db.fauna.com/tokens/self \
  -u fnEDfS4T34ACAAN9IwrU8aQA5SxTgyqYaUfiAqLqzQjQH9Qcr94:
HTTP Basic Auth wants credentials in the form "username:password". Since we’re using a secret that represents both, we just add a colon (:) to the end of the secret.

Running that command should show output similar to:

{
  "resource": {
    "ref": {
      "@ref": {
        "id": "251407817091580416",
        "class": { "@ref": { "id": "tokens" } }
      }
    },
    "ts": 1576020028130000,
    "instance": {
      "@ref": {
        "id": "251407645221585408",
        "class": {
          "@ref": {
            "id": "users",
            "class": { "@ref": { "id": "classes" } }
          }
        }
      }
    },
    "hashed_secret": "$2a$05$hljpg/MZ7FsbTv.5kIJP7umPKeuPr8Xwd0uWQ63KY/7ZPdUUwy1SO"
  }
}

If the secret that you use is invalid:

curl https://db.fauna.com/tokens/self \
  -u not_a_valid_secret:

You would see the following error:

{
  "errors": [ { "code": "unauthorized", "description": "Unauthorized" } ]
}

If your application is using one of the native Drivers, you should create a new client instance using the user’s token as the secret. Some drivers can create session clients in which the underlying HTTP connection is shared, so that you can intermingle queries using different tokens easily.

Multiple tokens can be created per user, which allows a user to log in from multiple sources.

User logout

When you call Logout, the token associated with the current session is invalidated, effectively logging out the user. A new token would need to be created for any future access.

Logout takes a single parameter all_tokens. When all_tokens is true, all tokens associated with the current user are invalidated, logging the user out completely. When all_tokens is false, only the current token is invalidated; any other active tokens are still valid.

You should only call Logout when connecting to FaunaDB with a token received from calling Login. In your client application code, that query would look similar to this JavaScript code:

client.query(q.Logout(true))

When you execute this query, a response of true indicates that log out was successful, and false indicates that log out failed.

Change a user’s password

You can change a user’s password by calling the Update or Replace functions with a new password in the credentials field.

When a password is updated, any existing tokens remain valid. If required, invalidate any previous session by calling Logout as described above.

Let’s change our user’s password. We are using the user ref, displayed when the user document was created. Copy the following query, paste it into the Shell, and run it:

Update(
  Ref(Collection("users"), "251407645221585408"),
  {
    credentials: { password: "new password" },
  }
)
You need to use the ref for the user that you created. The numerical portion of the ref that you see here differs from the value received from your query.

When you run this query, the result should be similar to:

{ ref: Ref(Collection("users"), "251407645221585408"),
  ts: 1576023407790000,
  data: { email: 'alice@site.example' } }

Let’s see if the original token still works:

curl https://db.fauna.com/tokens/self \
  -u fnEDfS4T34ACAAN9IwrU8aQA5SxTgyqYaUfiAqLqzQjQH9Qcr94:

And it does:

{
  "resource": {
    "ref": {
      "@ref": {
        "id": "251407817091580416",
        "class": { "@ref": { "id": "tokens" } }
      }
    },
    "ts": 1576020028130000,
    "instance": {
      "@ref": {
        "id": "251407645221585408",
        "class": {
          "@ref": {
            "id": "users",
            "class": { "@ref": { "id": "classes" } }
          }
        }
      }
    },
    "hashed_secret": "$2a$05$hljpg/MZ7FsbTv.5kIJP7umPKeuPr8Xwd0uWQ63KY/7ZPdUUwy1SO"
  }
}

Let’s get a new token based on the new password. Copy the following query, paste it into the Shell, and run it:

Login(
  Match(Index("users_by_email"), "alice@site.example"),
  { password: "new password" },
)

When you run this query, the result should be similar to:

{ ref: Ref(Tokens(), "251411540589150720"),
  ts: 1576023579110000,
  instance: Ref(Collection("users"), "251407645221585408"),
  secret: 'fnEDfTF20UACaan9IwQU8AIQiYcTZyxXaK9j91QCnhXc27TXoPQ' }
When you run the query, you will see different values. Make sure that you copy the value for the secret field; it is a token that authorizes access to FaunaDB, specifically to the associated database. It is only ever displayed once. If you lose it, a new token would have to be generated.

Let’s verify that the new token works:

curl https://db.fauna.com/tokens/self \
  -u fnEDfTF20UACaan9IwQU8AIQiYcTZyxXaK9j91QCnhXc27TXoPQ:

And it does:

{
  "resource": {
    "ref": {
      "@ref": {
        "id": "251411540589150720",
        "class": { "@ref": { "id": "tokens" } }
      }
    },
    "ts": 1576023579110000,
    "instance": {
      "@ref": {
        "id": "251407645221585408",
        "class": {
          "@ref": {
            "id": "users",
            "class": { "@ref": { "id": "classes" } }
          }
        }
      }
    },
    "hashed_secret": "$2a$05$l/WOFu6h9V3/vflDp6yGWOf/XDgCEJVG/G3JQmn6M9hzftYwivi0m"
  }
}

Check credentials

You can verify whether a user’s credentials are valid, without creating a token, by calling the Identify function.

Let’s test whether the old and new credentials for our user are valid. Copy the following query, paste it into the Shell, and run it:

[
  Identify(
    Ref(Collection("users"), "251407645221585408"),
    "secret password",
  ),
  Identify(
    Ref(Collection("users"), "251407645221585408"),
    "new password",
  ),
]

When you run this query, the result should be:

[ false, true ]

Third-party delegation

Third-party delegation is the scenario where a third party uses our APIs to provide services to our users.

Using the authentication features of FaunaDB, we can provide unique tokens for each third-party client that allow the third party to access resources on behalf of our users, while providing a way for the user to revoke the third-party client’s access.

First, we create an index that allows us to list all of a user’s tokens. Login allows us to attach data to a token by adding extra fields. We’ll use this capability to identify our tokens with the name of the third-party service that will use the tokens. Copy the following query, paste it into the Shell, and run it:

CreateIndex({
  name: "tokens_by_instance",
  permissions: { read: "public" },
  source: Tokens(),
  terms: [{ field: "instance" }],
  values: [{field: ["data", "name"]}]
})

When you run this query, the result should be similar to:

{ ref: Index("tokens_by_instance"),
  ts: 1576024400110000,
  active: false,
  serialized: true,
  name: 'tokens_by_instance',
  permissions: { read: 'public' },
  source: Tokens(),
  terms: [ { field: 'instance' } ],
  values: [ { field: [ 'data', 'name' ] } ],
  partitions: 1 }

Now we can create a token for each third-party service that our user uses. And we can do it all in a single query. Copy the following query, paste it into the Shell, and run it:

Map(
  [
    "Desktop App",
    "Mobile App",
    "Web Service"
  ],
  Lambda(
    "service",
    Login(
      Match(Index("users_by_email"), "alice@site.example"),
      {
        password: "new password",
        data: { name: Var("service") }
      }
    )
  )
)

When you run this query, the result should be similar to:

[ { ref: Ref(Tokens(), "251412696160797184"),
    ts: 1576024681170000,
    data: { name: 'Desktop App' },
    instance: Ref(Collection("users"), "251407645221585408"),
    secret: 'fnEDfTKD3rACAAN9IwrU8AIAa-IOXEqSP5rSkGjdQ_0eG9rBet0' },
  { ref: Ref(Tokens(), "251412696160799232"),
    ts: 1576024681170000,
    data: { name: 'Mobile App' },
    instance: Ref(Collection("users"), "251407645221585408"),
    secret: 'fnEDfTKD3rAKAAN9IwrU8AIA0su3H1YuSdUlgG5EQPRsqRcVyzQ' },
  { ref: Ref(Tokens(), "251412696160798208"),
    ts: 1576024681170000,
    data: { name: 'Web Service' },
    instance: Ref(Collection("users"), "251407645221585408"),
    secret: 'fnEDfTKD3rAGAAN9IwrU8AIAWe9UYsSvsOHgw0LHSnXj5CErYuo' } ]

Finally, in client application code, we can list all of the currently logged-in user’s tokens by querying the index that we built, when connecting to FaunaDB using the user’s token. The following code is written in JavaScript:

client.query(
  q.Paginate(
    q.Match(
      q.Index("tokens_by_instance"),
      q.Select("instance", q.Identity())
      )
    )
  )
)
.then((ret) => console.log(ret))
.catch((err) => console.log("Error:", err))

When you execute this query in your client application code, after the user has logged in successfully, the output should be:

{ data: [ 'Desktop App', 'Mobile App', 'Web Service' ] }

Was this article helpful?

We're sorry to hear that.
Tell us how we can improve! documentation@fauna.com

Thank you for your feedback!