Tokens and role membership

In this section, you write an simple application that uses the authentication functions you added to the CoffeeStore database. If you are working through the cookbook sequentially, you have already written simple Javascript application using the fauna module.

Application authentication

In this section, you update the coffeestore.mjs file to sign in and then log in a user.

  1. Edit the coffeestore.mjs file and simplify the connection section to this:

    import { Client, fql } from "fauna";
    
    const envSecret = process.env.FAUNA_SECRET;
    
    // Configure your client
    const client = new Client({ endpoint: new URL("https://db.fauna.com"),
    secret: envSecret});

    The application uses an admin secret to make the initial connection to Fauna.

  2. Replace the try/catch block with the following code.

    try {
    
      // Build queries using the fql function
    
      // Sign a user up
      const signup_query = fql`Signup("Hector Binding", "hbinding@gmail.com", "sekret")`;
    
      // execute the query
      const signup_result = await client.query(signup_query);
      console.log("CREDENTIAL created:");
      console.log(signup_result);
    
    
      // Log the user in and get back the token
      const login_query = fql`loginUser("hbinding@gmail.com", "sekret")`;
      const login_result = await client.query(login_query);
    
      console.log("TOKEN created:");
      console.log(login_result);
    
      // Return the token secret for testing purposes
      const just_data = login_result.data;
      console.log("Token SECRET: " + just_data.secret);
    
      // Make a new client with the user token const loginClient = new Client({
      const loginClient = new Client({ endpoint: new URL("https://db.fauna.com"), secret: just_data.secret});
    
      // Create a query to locate the user token
      const token_query = fql`Query.token()`;
    
      //Return the token
      const token_result = await loginClient.query(token_query);
      console.log("Query.token() returns:");
      console.log(token_result);
    
    } catch (error) {
      console.log(error)
    }

    This code authenticates users in a server-side JavaScript application. You would use a similar approach if you’re using Fauna with other programming languages.

  3. Run the application:

    node coffeestore.mjs

    You should response similar to the following:

    CREDENTIAL created:
    {
      data: Document {
        coll: Module { name: 'Credential' },
        id: '372736323917709345',
        ts: TimeStub { isoString: '2023-08-11T04:25:08.950Z' },
        document: DocumentReference { coll: [Module], id: '372736323915612193' }
      },
      static_type: 'Credential',
      summary: '',
      txn_ts: 1691727908950000,
      stats: {
        compute_ops: 1,
        read_ops: 0,
        write_ops: 2,
        query_time_ms: 92,
        contention_retries: 0,
        storage_bytes_read: 601,
        storage_bytes_write: 808,
        rate_limits_hit: []
      },
      schema_version: 1691691883450000
    }
    TOKEN created:
    {
      data: Document {
        coll: Module { name: 'Token' },
        id: '372736324061364257',
        ts: TimeStub { isoString: '2023-08-11T04:25:09.090Z' },
        secret: 'fnEFLDm2PpAAIQUh3mNk4AAh-t4fkze2d-ZU8YI4C0tK2PxEMmk',
        document: DocumentReference { coll: [Module], id: '372736323915612193' }
      },
      static_type: 'Token',
      summary: '',
      txn_ts: 1691727909090000,
      stats: {
        compute_ops: 1,
        read_ops: 2,
        write_ops: 1,
        query_time_ms: 89,
        contention_retries: 0,
        storage_bytes_read: 791,
        storage_bytes_write: 433,
        rate_limits_hit: []
      },
      schema_version: 1691691883450000
    }
    Token SECRET: fnEFLDm2PpAAIQUh3mNk4AAh-t4fkze2d-ZU8YI4C0tK2PxEMmk
    Query.token() returns:
    {
      data: Document {
        coll: Module { name: 'Token' },
        id: '372736324061364257',
        ts: TimeStub { isoString: '2023-08-11T04:25:09.090Z' },
        document: DocumentReference { coll: [Module], id: '372736323915612193' }
      },
      static_type: '{ *: Any } | Null',
      summary: '',
      txn_ts: 1691727909139652,
      stats: {
        compute_ops: 1,
        read_ops: 1,
        write_ops: 0,
        query_time_ms: 6,
        contention_retries: 0,
        storage_bytes_read: 597,
        storage_bytes_write: 0,
        rate_limits_hit: []
      },
      schema_version: 1691691883450000
    }

    In the output, you can see that the 372736323915612193 document id is associated with the credential and the token. This is the id belonging to the People document associated with the token.

  4. Go into the Dashboard Shell and query for the user referenced by the token.

    People.byId("372736323915612193")
    {
      id: "372736323915612193",
      coll: People,
      ts: Time("2023-08-11T04:25:08.950Z"),
      name: "Hector Binding",
      email: "hbinding@gmail.com"
    }
  5. You can also find the Credential for this user.

    People.byId("372736323915612193")
    {
      id: "372736323915612193",
      coll: People,
      ts: Time("2023-08-11T04:25:08.950Z"),
      name: "Hector Binding",
      email: "hbinding@gmail.com"
    }

Like a token, this document never expires. You can pass a ttl value to the Credential.login() call to expire the token.

Token secret storage

After your code obtains the token secret for the user, you can use it for subsequent queries to Fauna on behalf of the user. Because this code did not set the ttl value, its default value is null, which causes the document to persist indefinitely or until deleted.

You have many options for storing the secret:

  • Pure client-side: If you intend on accessing Fauna client-side, you could send the secret back to the client and store it in memory.

  • Partial backend with cookie: If you are working on a server API, you could store the secret in a session and send it back to the client using a secure cookie.

  • Partial backend with httpOnly cookie: Combine the preceding two approaches by creating two types of tokens in Fauna. One that could be used as a refresh token and stored in an httpOnly cookie, and another short-lived one that could be used and stored in the frontend.

  • Entirely backend: You could also decide that you never want your clients receiving the secret and store the session in some cache and send back a session ID.

These examples have different security implications which are far too vast and complex to discuss here. Also, solutions depend on your application use case. Consider carefully which of the solution is best for you.

Role membership

When a query is made with Token, Fauna evaluates the stored Credential to authenticate a caller. Role membership and privileges are evaluated to grant authorization. Recall the humanResources role currently has this configuration:

{
  name: "humanResources",
  coll: Role,
  ts: Time("2023-07-21T00:22:25.260Z"),
  privileges: {
    resource: "People",
    actions: {
      read: true,
      create: "data => data.employment == \'active\' "
    }
  }
}

Anyone holding a key with the humanResources role can read the People collection and create an element in it as long as the element is active.

The secret, fnEFLDm2PpAAIQUh3mNk4AAh-t4fkze2d-ZU8YI4C0tK2PxEMmk, for the Hector user cannot read the People collection. You can test this.

  1. Go to the Dashboard Shell and run the People.all() query as Admin.

    If you are working through this cookbook, the People collection should have several documents.

  2. Run the query again with the secret your application returned for Hector.

    You only see the Hector user. The user token can only see the document whose id matches the one found in the corresponding credential. To allow Hector to see the complete People collection, you must update the humanResources role.

  3. Switch to an Admin key in the shell, update the role.

    Role.byName("humanResources")!.update(
        {
          name: "humanResources",
          membership: [
            {resource: "People"}],
          privileges: {
            resource: "People",
            actions: {
              read: true,
              create: "data => data.employment == \'active\' "
            }
          }
        })
    {
      name: "humanResources",
      coll: Role,
      ts: Time("2023-08-11T05:27:52.160Z"),
      privileges: {
        resource: "People",
        actions: {
          read: true,
          create: "data => data.employment == \'active\' "
        }
      },
      membership: [
        {
          resource: "People"
        }
      ]
    }

    This update specifies a membership which grants everyone in People the ability to list all the documents in the collection.

  4. Switch the Shell back to the secret your application returned for Hector.

  5. Run the People.all() query again.

    This time Hector should see all the people.

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!