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 in to the Fauna Dashboard and create a database. When creating the database, enable demo data.

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

    fauna cloud-login

    When prompted, enter:

    • Endpoint name: cloud (Press Enter)

      An endpoint defines the settings the CLI uses to run API requests against a Fauna account or database. See Endpoints.

    • Email address: The email address for your Fauna account.

    • Password: The password for your Fauna account.

    • Which endpoint would you like to set as default? The cloud-* endpoint for your preferred Region Group. For example, to use the US Region Group, use cloud-us.

    fauna cloud-login requires an email and password login. If you log in to 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 directory:

    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 active schema to the local schema directory:

    fauna schema pull

    When prompted, accept and pull the changes.

  6. Navigate to the schema directory:

    cd schema
  7. To simplify the tutorial, open collections.fsl and edit the Customer collection as follows:

    collection Customer {
      // Make `name` nullable.
      name: String?
      email: String
      // Make `address` nullable.
      address: {
        street: String
        city: String
        state: String
        postalCode: String
        country: String
      }?
    
    // Leave remaining schema as is.

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. In the schema directory, add the following function schema 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
      // query's authentication token.
      Query.token()!.delete()
    }

Create a 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 as follows:

  • Create a customer role with the ability to call the UserLogOut() function.

  • Create a unauthenticated role with the ability to call the UserSignup() and UserLogin() functions.

  1. In the schema directory, create the roles.fsl file and add the following schema to it:

    // Defines the `customer` role.
    role customer {
      // Assign the `customer` role to tokens with
      // an identity document in the `Customer` collection.
      membership Customer
    
      // Grants `read` access to `Product` collection documents.
      privileges Product {
        read
      }
    
      // Grants the ability to call the `UserLogout()` UDF.
      privileges UserLogout {
        call
      }
    }
    
    // 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
      }
    }
  2. Save collections.fsl, functions.fsl, and roles.fsl. Then push the schema to Fauna:

    fauna schema push

    When prompted, accept and stage the schema.

  3. Check the status of the staged schema:

    fauna schema status
  4. When the status is ready, commit the staged schema to the database:

    fauna schema commit

    The commit applies the staged schema to the database. 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: "111",
          coll: Product,
          ts: Time("2099-07-31T13:29:18.350Z"),
          name: "cups",
          description: "Translucent 9 Oz, 100 ct",
          price: 698,
          stock: 100,
          category: Category("123")
        },
        ...
      ]
    }
  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!