Build an end-user authentication system

Learn: Credentials

You can use credentials and user-defined functions (UDFs) to create an end-user authentication system.

In this tutorial, you’ll build a user authentication system for an example e-commerce application. The system follows the principle of least privilege by:

  • Limiting the privileges of roles assigned to a user’s tokens

  • Only allowing unauthenticated users to access data through UDFs

Before you start

To complete this tutorial, you’ll need:

Setup

Set up a database with demo data.

  1. Log into Fauna Dashboard and create a database. When creating the database, enable demo data.

  2. In your terminal, log into Fauna using the Fauna CLI:

    fauna cloud-login

    cloud-login requires an email and password login. If you log in to the Fauna using GitHub or Netlify, you can enable email and password login using the Forgot Password workflow.

  3. Create an ecommerce directory and navigate to it:

    mkdir ecommerce
    cd ecommerce
  4. Initialize a project:

    fauna project init

    When prompted, use:

    • schema as the schema directory. The command creates the directory.

    • dev as the environment name.

    • The default endpoint.

    • Your demo database as the database.

  5. Pull the database’s live schema to the local schema directory:

    fauna schema pull

    When prompted, accept and pull the changes.

Create user login functions

Create UDFs that let end users:

  • Sign up as application users by providing an email address and password

  • Log in to the application using their email and password

  • Log out of the application

  1. Navigate to the schema directory:

    cd schema
  2. Add the following function schemas to the end of functions.fsl:

    ...
    
    // Defines the `UserSignup()` UDF. Unauthenticated users can use
    // the UDF to create a `Customer` identity document and credential.
    @role("server") // Runs with the `server` role's privileges
    function UserSignup(email, password) {
      // Creates a `Customer` collection document.
      // The `Customer` document acts as an identity document that
      // represents the end user.
      let customer = Customer.create({
        email: email
      })
    
      // Creates a credential that associates the
      // `Customer` document with an end user's password.
      let credential = Credential.create({
        document: customer,
        password: password
      })
    
      // Outputs the credential as an object.
      Object.assign({  }, credential)
    }
    
    // Defines the `UserLogin()` UDF. Unauthenticated users can use the
    // UDF to create an token associated with their identity document.
    @role("server") // Runs with the `server` role's privileges.
    function UserLogin(email, password) {
      // Uses the `Customer` collection's `byEmail()` index to
      // get `Customer` collection documents by `email` field value.
      // In the `Customer` collection, `email` field values are unique
      // so return the `first()` (and only) document.
      let customer = Customer.byEmail(email)?.first()
    
      // Gets the credential for the above `Customer` document and
      // passes it to `login()` to create a token.
      // The `Customer` document is the token's identity document.
      // Set the token's `ttl` to 60 minutes from the current time at
      // query. The token's secret expires at its `ttl`.
      Credential.byDocument(customer)
        ?.login(password, Time.now().add(60, "minutes"))
    }
    
    
    // Defines the `UserLogout()` UDF. Authenticated users can use the
    // UDF to delete their authentication token. The user creates a
    // new token when they next log in.
    function UserLogout() {
      // `Query.token()` gets the token document for the
      // transaction's authentication token.
      Query.token()!.delete()
    }

Update user-defined roles

Unauthenticated users should only be able to sign up or log in to the application.

Authenticated users should have read access to the application’s products. Authenticated users should also be able to log out of the application.

Update the database’s user-defined roles as follows:

  • Update the existing customer role to grant the ability to call the UserLogOut() function.

  • Create a user-defined role for unauthenticated users.

  1. In the schema directory, open roles.fsl and edit the existing Customer role schema as follows:

    ...
    role customer {
      // Assign the `customer` role to tokens with
      // an identity document in the `Customer` collection.
      membership Customer
    
      // Grants `read` access to `Store` collection documents.
      privileges Store {
        read
      }
    
      // Grants `read` access to `Product` collection documents.
      privileges Product {
        read
      }
    
      // Grants the ability to call the `UserLogout()` UDF.
      privileges UserLogout {
        call
      }
    
      // Leave remaining privileges as is.
      ...
    }
  2. Add the following role schema to the end of roles.fsl:

    ...
    
    // Defines the `unauthenticated` role.
    role unauthenticated {
      // Grants the ability to call the `UserSignup()` UDF.
      privileges UserSignup {
        call
      }
    
      // Grants the ability to call the `UserLogin()` UDF.
      privileges UserLogin {
        call
      }
    }
  3. Save functions.fsl and roles.fsl. Then push the schema to Fauna:

    fauna schema push

    When prompted, accept and push the changes. This creates the new UDFs and role.

Create a key for unauthenticated users

Create a key that’s assigned the unauthenticated role.

The key acts as a bootstrap to let unauthenticated users sign up or log in to the application. You’d typically store the key’s secret in the FAUNA_SECRET environment variable for use in a Fauna client driver.

  1. Start a shell session in the Fauna CLI:

    fauna shell

    The shell runs with the default admin key you created when you logged in using fauna cloud-login.

  2. Run the following FQL query:

    Key.create({ role: "unauthenticated" })

    Copy the returned key’s secret. You’ll use the secret later in the tutorial.

  3. Press Ctrl+D to exit the shell.

Test unauthenticated user access

Use the key’s secret to run queries as an unauthenticated user.

The key only has privileges for the login-related UDFs you created earlier. The key shouldn’t have access to product data.

  1. Start a new shell session using the key’s secret:

    fauna shell --secret <KEY_SECRET>
  2. Enter editor mode to run multi-line queries:

    > .editor
  3. Run the following query:

    // Attempt to read `Product` collection documents.
    Product.all()

    The query returns an empty set:

    {
      data: []
    }

    The unauthenticated role doesn’t grant access to Product collection documents.

Sign up and log in as an end user

Use the key to create a token tied to a user’s Customer identity document.

Once created, the application can store the token’s secret and use it to authenticate Fauna queries on behalf of the user.

  1. In the shell session, run the following query in editor mode:

    // Calls the `UserSignup()` UDF. Passes the `email` for the
    // `Customer` identity document you created earlier as the first
    // argument. Passes the user's password as the second argument.
    UserSignup("john.doe@example.com", "sekret")

    The results contains a permission-denied message. This is expected. The unauthenticated role doesn’t have read access to Customer documents. The function call still creates the related credential.

  2. Run the following query in editor mode:

    // Calls the `UserLogin()` UDF. Passes the same arguments as the
    // earlier `UserSignup()` UDF call.
    UserLogin("john.doe@example.com", "sekret")

    Copy the returned token’s secret. You’ll use the secret later in the tutorial.

  3. Press Ctrl+D to exit the shell.

Test authenticated user access

The user’s token can access product data. Use the token’s secret to access the data and then log out.

  1. Start a new shell session using the token secret:

    fauna shell --secret <TOKEN_SECRET>
  2. Run the following query:

    Product.all()

    The query runs successfully:

    {
      data: [
        {
          id: "401682422130802761",
          coll: Product,
          ts: Time("2099-06-25T16:30:59.430Z"),
          name: "cups",
          description: "Translucent 9 Oz, 100 ct",
          price: 6.98,
          quantity: 90,
          store: Store("401682422127657033"),
          backorderLimit: 5,
          backordered: false
        },
        ...
      ]
    }
  3. Run the following query in editor mode:

    // Calls the `UserLogout()` UDF. The call deletes the authentication
    // token and its secret. Users can create a new token and secret by
    // logging in again using `UserLogin()`.
    UserLogout()

    The query runs successfully and indicates the token was deleted.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!