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:
-
The Fauna CLI
-
Familiarity with the following Fauna features:
Setup
Set up a database with demo data.
-
Log in to the Fauna Dashboard and create a database. When creating the database, enable demo data.
-
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, usecloud-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. -
-
Create an
ecommerce
directory and navigate to it:mkdir ecommerce cd ecommerce
-
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.
-
-
Pull the database’s active schema to the local schema directory:
fauna schema pull
When prompted, accept and pull the changes.
-
Navigate to the
schema
directory:cd schema
-
To simplify the tutorial, open
collections.fsl
and edit theCustomer
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
-
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 theUserLogOut()
function. -
Create a
unauthenticated
role with the ability to call theUserSignup()
andUserLogin()
functions.
-
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 } }
-
Save
collections.fsl
,functions.fsl
, androles.fsl
. Then push the schema to Fauna:fauna schema push
When prompted, accept and stage the schema.
-
Check the status of the staged schema:
fauna schema status
-
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.
-
Start a shell session in the Fauna CLI:
fauna shell
The shell runs with the default
admin
key you created when you logged in usingfauna cloud-login
. -
Run the following FQL query:
Key.create({ role: "unauthenticated" })
Copy the returned key’s
secret
. You’ll use the secret later in the tutorial. -
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.
-
Start a new shell session using the key’s secret:
fauna shell --secret <KEY_SECRET>
-
Enter editor mode to run multi-line queries:
> .editor
-
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 toProduct
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.
-
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 toCustomer
documents. The function call still creates the related credential. -
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. -
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.
-
Start a new shell session using the token secret:
fauna shell --secret <TOKEN_SECRET>
-
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") }, ... ] }
-
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!