User authentication in Fauna (an opinionated guide)
⚠️ Disclaimer ⚠️
This post refers to a previous version of FQL.
This post refers to a previous version of FQL (v4). For the most current version of the language, visit our FQL documentation.
In this blog post, you learn the fundamentals of authenticating users in Fauna using UDFs. You ship your client applications with a secret token from Fauna that has limited privileges. Ideally, this token can only register and login users in Fauna. Authenticated users then receive a temporary access token that they can use to access the Fauna resources securely. User-defined functions (UDFs) are the key to this implementation.
For more about advanced Authentication strategies (i.e., token refresh) in Fauna, review this post. If you are interested in using third-party identity providers with Fauna, take a look at the Fauna and Auth0 integration post. Want to use AWS Cognito as an auth provider with Fauna? Take a look at the AWS Cognito and Fauna article.
Pre-requisites
Some familiarity with FQL will be helpful. You can still follow along without any prior knowledge of FQL. To learn more about FQL visit this series of articles .
Solution overview
In this post, you:
- Create a new secret key in Fauna.
- Configure a role for the secret key so that your client application can only invoke the User Registration and Login UDFs using this key.
- Ship the secret key as an environment variable with your client application.
- Run the User Registration UDF from the client application using the secret key to create a new user.
- Run the Login UDF from the client application to acquire a user access token.
- Use the user access token in the client application to interact with Fauna resources.
The following diagram demonstrates the overall authentication flow.
User registration
Head over to the Fauna dashboard and create a new database.
Select Collections and create a new collection called
Account
The
Account
collection contains all user data. Navigate to Indexes and select New Index. Select Account
as the source collection. Name the index account_by_email
. You use this index to query users by their email address. In the Terms field, input email
. Make sure to select the Unique option and the Serialized option to ensure that each user has a unique email address. Select Save to create the new index. Next, create a function to register new users. Select Functions from the dashboard menu and select NEW FUNCTION to create a new user-defined function (UDF).
Name your function
UserRegistration
and enter the following code in the Function Body. You can leave the role as None for now. Select Save to create your function.Query(
Lambda(["email", "password"],
Create(Collection("Account"), {
credentials: { password: Var("password") },
data: {
email: Var("email")
}
})
)
)
Navigate to the Shell and register a new user by calling the
UserRegistration
UDF. Enter the following code in the shell and select Run Query.Call("UserRegistration", "shadid@test.com", "pass123456")
Navigate back to Collections > Account and confirm that a new user is created.
User login
Return to the Functions tab, create a new function, and name it
UserLogin
. Add the following code snippet to your function body and select Save. Notice there is a ttl
argument in the Login
function. This ensures that the generated token expires after the specified time.Query(
Lambda(["email", "password"],
Login(
Match(Index("account_by_email"), Var("email")),
{
password: Var("password"),
ttl: TimeAdd(Now(), 3600, "seconds")
},
)
)
)
Navigate back to the shell and call the function with the user's credentials.
Call("UserLogin", "shadid@test.com", "pass123456")
The output of this function gives you a secret token. Following is a sample response from the UDF. Take a note of the secret.
{
ref: Ref(Ref("tokens"), "310382039289299523"),
ts: 1632262229150000,
ttl: Time("2021-09-21T22:20:29.019666Z"),
instance: Ref(Collection("Account"), "310358409884992069"),
secret: "fnEE....."
}
You can now use this secret token to access your Fauna resources from a client application. Verify the validity of the token by running the following
CURL
command in your terminal, replacing <YOUR_TOKEN>
with the value of the secret your function returns.curl https://db.fauna.com/tokens/self -u <YOUR_TOKEN>
{
"resource": {
"ref": {
"@ref": {
"id": "310382039289299523",
"class": { "@ref": { "id": "tokens" } }
}
},
"ts": 1632262229150000,
"ttl": { "@ts": "2021-09-21T22:20:29.019666Z" },
"instance": {
"@ref": {
"id": "310358409884992069",
"class": {
"@ref": {
"id": "Account",
"class": { "@ref": { "id": "classes" } }
}
}
}
},
"hashed_secret": "$2a$05$Ta2ScWEQhV39VTKLmmXXXOxnpQlXvloLSd3g9nP2Gsb.zJMP6QK6y"
}
}
Next, navigate to Collections and create a new collection called
Movie
. Populate your collection with the following sample data.{
"title": "Reservoir Dogs",
"director": "Quentin Tarantino",
"release": "Jan 21, 1992"
}
{
"title": "The Hateful Eight",
"director": "Quentin Tarantino",
"release": "December 25, 2015"
}
{
"title": "Once Upon a Time in Hollywood",
"director": "Quentin Tarantino",
"release": "July 26, 2019"
}
You will query this collection with the authenticated user's token in the next section.
Connecting Fauna with the client application
Your client application should have limited access to your Fauna backend. An unauthenticated user should only be able to call
UserLogin
and UserRegistration
functions from your client application. It is best practice to follow the principle of least privilege .Create a new role for your unauthenticated users. Navigate to Security > Roles and select New Custom Role.
Name your role
UnAuthRole
. Give the privilege to execute UserLogin
and UserRegistration
. As your functions are using the account_by_email
index, provide read access to account_by_email
as well. Also, provide read and create access to Account collection because UserLogin
and UserRegistration
UDFs need to read and create permission to this collection.Next, generate a security key for your
UnAuthRole
. Navigate to Security > Keys > New Key.Make sure to select
UnAuthRole
from the role options for your new key.You ship this key as an environment variable in your client application. Your client application can use this key to call only the
UserRegistration
and UserLogin
function. You can not access any other resources in Fauna with this key.To test this navigate to the Shell from the Fauna dashboard and select Run Query As > Specify a Secret Option. In the secret field input your key.
Run the following command in the shell.
Call("UserRegistration", "john@gmail.com", "pass12345"")
Call("UserRegistration", "john@gmail.com", "pass12345")
{
ref: Ref(Collection("Account"), "311012249060770373"),
ts: 1632863244095000,
data: {
email: "john@gmail.com"
}
}
Notice, that this will register a new user. Try accessing any other resources (i.e. Movie collection) using the same key. Run the following command in the shell.
Get(Documents(Collection("Movie")))
// Output
Error: [
{
"position": [],
"code": "permission denied",
"description": "Insufficient privileges to perform the action."
}
]
Review the output in the shell. You get an
"Insufficient privileges to perform the action."
error. It throws this error because the specified key doesn’t have the privilege to access the Movie
collection. This means the key is working as intended.Next, run the
UserLogin
function in the shell.Call("UserLogin", "john@gmail.com", "pass12345")
Review the output. The function returned a secret.
{
ref: Ref(Ref("tokens"), "311013644284461635"),
ts: 1632864574726000,
ttl: Time("2021-09-28T21:39:34.245802Z"),
instance: Ref(Collection("Account"), "311012249060770373"),
secret: "fnEEUPFC--ACQwROmU7CwAZDlUcF9R3liL1iaNf9y80UQgc_qLI"
}
This secret is your temporary access token that can access other resources in Fauna. However, when you try to use it now to access the
Movie
collection you get the same error. This is because you have not yet defined what resources does this token has access to. In the next section, you learn how to give your user access tokens permission to certain resources.Authenticated user role
Navigate to Security > Roles > New Role to Create a new user role. Name your role
AuthRole
and provide read
, write
, and create
privileges on the Movie
collection.Select the Membership tab and add the Account collection as a member.
Run the
UserLogin
function again with the same credentials. Input the generated secret (user access token) next to the Specify a secret field in the shell.Run the following command to query
Movie
collection. Notice this time it returns a successful response.Get(Documents(Collection("Movie")))
// Output
{
ref: Ref(Collection("Movie"), "310384575062737476"),
ts: 1632264647455000,
data: {
title: "The Hateful Eight",
director: "Quentin Tarantino",
release: "December 25, 2015"
}
}
User logout
Navigate to the function section of your Fauna dashboard. Define a new UDF called
UserLogout
and add the following code snippet in the function body.Query(
Lambda("x", Logout(true))
)
Make sure to assign AuthRole to UserLogout UDF.
Navigate to Security > Roles > AuthRole and assign call privilege to UserLogout UDF.
Calling this UDF invalidates your secret token. You call this function with the following command.
Call("UserLogout")
If you enjoyed this article and want to see more articles like this one, let us know in the community forum.
If you enjoyed our blog, and want to work on systems and challenges related to globally distributed systems, and serverless databases, Fauna is hiring
Subscribe to Fauna's newsletter
Get latest blog posts, development tips & tricks, and latest learning material delivered right to your inbox.