User authentication
This tutorial assumes that you have completed the Quick Start with FaunaDB tutorial. |
FaunaDB offers built-in identity, authentication, and password management. This tutorial walks you through how to create user identities, authenticate them, and manage their sessions.
This tutorial is divided into several sections:
Setup
This setup section describes all of the preparatory work we need to do to prepare for authenticating our users. It includes:
Create a database
When we want to authenticate users, it is typically in the context of a specific application. With that in mind, let’s create an application-specific database called "app1". Copy the following query, paste it into the Shell, and run it:
CreateDatabase({
name: "app1"
})
When you run this query, the result should be similar to:
{ ref: Database("app1"), ts: 1576008456740000, name: 'app1' }
Create a server key
Our application is going to need access to our new database. We don’t want to give it permission to every database, so let’s create a "server" key, which provides full access to a specific database. Copy the following query, paste it into the Shell, and run it:
CreateKey({
name: "Server key for app1",
database: Database("app1"),
role: "server",
})
When you run this query, the result should be similar to:
{ ref: Ref(Keys(), "251405600267698688"),
ts: 1576017914170000,
name: 'Server key for app1',
database: Database("app1"),
role: 'server',
secret: 'fnADfSwPQoACAFAfWX9f6NFrBumWqIMsL8Qkt3wY',
hashed_secret:
'$2a$05$pC8bzQqEw3EM4TbsPJ6tjOzqnTV2rRZEQhT7sxI1SsdkCfk14n9qq' }
When you run the query, you will see different values.
Make sure that you copy the value for the secret field;
it is a key that authorizes access to FaunaDB, specifically
to the associated database. It is only ever displayed
once. If you lose it, a new key would have to be
generated.
|
Create a client key
We need to allow our application’s public clients, typically a web browser, to access our "app1" database, and we need to embed a key into the public client to permit that access. So, let’s create a "client" key. Copy the following query, paste it into the Shell, and run it:
CreateKey({
name: "Client key for app1",
database: Database("app1"),
role: "client",
})
When you run this query, the result should be similar to:
{ ref: Ref(Keys(), "251406008027447808"),
ts: 1576018302900000,
name: 'Client key for app1',
database: Database("app1"),
role: 'client',
secret: 'fnADfSxuQuAQAe6JBbRY58Fm37ZA-0HhFtVm64T0',
hashed_secret:
'$2a$05$qKd8N/LsdLQ9kQKGmtYa/OjgNCQNlzG5sNO9xT1jWSiBuPMNGREJW' }
When you run the query, you will see different values.
Make sure that you copy the value for the secret field;
it is a key that authorizes access to FaunaDB, specifically
to the associated database. It is only ever displayed
once. If you lose it, a new key would have to be
generated.
|
Create a collection to store user documents
Now that we have our app-specific database, and keys to access it, now we can create a collection where we can store user documents.
Let’s use our server key to access the new database. First, type .exit
into the Shell and press Return.
Then start the shell using the secret for the server key:
fauna shell --secret=fnADfSwPQoACAFAfWX9f6NFrBumWqIMsL8Qkt3wY
Be sure to replace fnADfSwPQoACAFAfWX9f6NFrBumWqIMsL8Qkt3wY
with the secret that you acquired for the server key.
|
Now, let’s create the collection to store users. Copy the following query, paste it into the Shell, and run it:
CreateCollection({ name: "users" })
When you run this query, the result should be similar to:
{ ref: Collection("users"),
ts: 1576019188350000,
history_days: 30,
name: 'users' }
Create a public index for our users
We need an index to make it possible to lookup our users by their email address. We need this index to be public, since unauthenticated users would be using the client key when they attempt to login. So, let’s create the index. Copy the following query, paste it into the Shell, and run it:
CreateIndex({
name: "users_by_email",
permissions: { read: "public"},
source: Collection("users"),
terms: [{field: ["data", "email"]}],
unique: true,
})
When you run this query, the result should be similar to:
{ ref: Index("users_by_email"),
ts: 1576019648660000,
active: false,
serialized: true,
name: 'users_by_email',
permissions: { read: 'public' },
source: Collection("users"),
terms: [ { field: [ 'data', 'email' ] } ],
unique: true,
partitions: 1 }
At this point, the setup is complete!
Create users
When a new user signs up, we can create a new user document that contains their email address and password. More specifically, a hash of the user’s password is stored; FaunaDB does not store credentials in plain text.
Let’s create our first user. Copy the following query, paste it into the Shell, and run it:
Create(
Collection("users"),
{
credentials: { password: "secret password" },
data: {
email: "alice@site.example",
},
}
)
When you run this query, the result should be similar to:
{ ref: Ref(Collection("users"), "251407645221585408"),
ts: 1576019864330000,
data: { email: 'alice@site.example' } }
User login
When a user wants to login, they would provide their email address and
password. Then we use the Login
function to authenticate their access, and if valid, provide them with a
token that they can use to access resources.
A token only provides access according to the privileges granted by an attribute-based access control (ABAC) role. These differ from keys, which are used to provide coarser, database-level access. |
The following query calls Login
on the result of looking up the user
via the users_by_email
index, with the password that they provided.
Copy the query, paste it into the Shell, and run it:
Login(
Match(Index("users_by_email"), "alice@site.example"),
{ password: "secret password" },
)
When you run this query, the result should be similar to:
{ ref: Ref(Tokens(), "251407817091580416"),
ts: 1576020028130000,
instance: Ref(Collection("users"), "251407645221585408"),
secret: 'fnEDfS4T34ACAAN9IwrU8aQA5SxTgyqYaUfiAqLqzQjQH9Qcr94' }
When you run the query, you will see different values.
Make sure that you copy the value for the secret field;
it is a token that authorizes access to FaunaDB, specifically
to the associated database. It is only ever displayed
once. If you lose it, a new token would have to be
generated.
|
If the user cannot be found, or if their credentials are invalid, an error would be returned:
Login(
Match(Index("users_by_email"), "bob@not.a.member"),
{ password: "secret password" },
)
[ { position: [],
code: 'authentication failed',
description:
'The document was not found or provided password was incorrect.' } ]
The token provided for a successful login is all that is required to perform authenticated queries; it represents both the identity and authorization for the user. The token can now be used in any subsequent queries for resources.
Your app should use the value in the secret
field to create another
client instance, which should be used to perform queries as that user.
If your application is using HTTP requests to interact with FaunaDB, you
can use the token as a username+password via the Basic-Auth
header,
for every query made by that specific user. For example, you could use
curl
:
curl https://db.fauna.com/tokens/self \
-u fnEDfS4T34ACAAN9IwrU8aQA5SxTgyqYaUfiAqLqzQjQH9Qcr94:
HTTP Basic Auth wants credentials in the form "username:password".
Since we’re using a secret that represents both, we just add a
colon (: ) to the end of the secret.
|
Running that command should show output similar to:
{
"resource": {
"ref": {
"@ref": {
"id": "251407817091580416",
"class": { "@ref": { "id": "tokens" } }
}
},
"ts": 1576020028130000,
"instance": {
"@ref": {
"id": "251407645221585408",
"class": {
"@ref": {
"id": "users",
"class": { "@ref": { "id": "classes" } }
}
}
}
},
"hashed_secret": "$2a$05$hljpg/MZ7FsbTv.5kIJP7umPKeuPr8Xwd0uWQ63KY/7ZPdUUwy1SO"
}
}
If the secret that you use is invalid:
curl https://db.fauna.com/tokens/self \
-u not_a_valid_secret:
You would see the following error:
{
"errors": [ { "code": "unauthorized", "description": "Unauthorized" } ]
}
If your application is using one of the native
Drivers, you should create a new client
instance using the user’s token as the secret
. Some drivers can create
session clients in which the underlying HTTP connection is shared, so
that you can intermingle queries using different tokens easily.
Multiple tokens can be created per user, which allows a user to log in from multiple sources.
User logout
When you call Logout
, the token
associated with the current session is invalidated, effectively logging
out the user. A new token would need to be created for any future
access.
Logout
takes a single parameter all_tokens
. When all_tokens
is
true
, all tokens associated with the current user are invalidated,
logging the user out completely. When all_tokens
is false
, only the
current token is invalidated; any other active tokens are still valid.
You should only call Logout
when connecting to FaunaDB with a token
received from calling Login
. In your client application code, that
query would look similar to this JavaScript code:
client.query(q.Logout(true))
When you execute this query, a response of true
indicates that log out
was successful, and false
indicates that log out failed.
Change a user’s password
You can change a user’s password by calling the
Update
or
Replace
functions with a new
password in the credentials
field.
When a password is updated, any existing tokens remain valid. If
required, invalidate any previous session by calling Logout
as
described above.
Let’s change our user’s password. We are using the user ref, displayed when the user document was created. Copy the following query, paste it into the Shell, and run it:
Update(
Ref(Collection("users"), "251407645221585408"),
{
credentials: { password: "new password" },
}
)
You need to use the ref for the user that you created. The numerical portion of the ref that you see here differs from the value received from your query. |
When you run this query, the result should be similar to:
{ ref: Ref(Collection("users"), "251407645221585408"),
ts: 1576023407790000,
data: { email: 'alice@site.example' } }
Let’s see if the original token still works:
curl https://db.fauna.com/tokens/self \
-u fnEDfS4T34ACAAN9IwrU8aQA5SxTgyqYaUfiAqLqzQjQH9Qcr94:
And it does:
{
"resource": {
"ref": {
"@ref": {
"id": "251407817091580416",
"class": { "@ref": { "id": "tokens" } }
}
},
"ts": 1576020028130000,
"instance": {
"@ref": {
"id": "251407645221585408",
"class": {
"@ref": {
"id": "users",
"class": { "@ref": { "id": "classes" } }
}
}
}
},
"hashed_secret": "$2a$05$hljpg/MZ7FsbTv.5kIJP7umPKeuPr8Xwd0uWQ63KY/7ZPdUUwy1SO"
}
}
Let’s get a new token based on the new password. Copy the following query, paste it into the Shell, and run it:
Login(
Match(Index("users_by_email"), "alice@site.example"),
{ password: "new password" },
)
When you run this query, the result should be similar to:
{ ref: Ref(Tokens(), "251411540589150720"),
ts: 1576023579110000,
instance: Ref(Collection("users"), "251407645221585408"),
secret: 'fnEDfTF20UACaan9IwQU8AIQiYcTZyxXaK9j91QCnhXc27TXoPQ' }
When you run the query, you will see different values.
Make sure that you copy the value for the secret field;
it is a token that authorizes access to FaunaDB, specifically
to the associated database. It is only ever displayed
once. If you lose it, a new token would have to be
generated.
|
Let’s verify that the new token works:
curl https://db.fauna.com/tokens/self \
-u fnEDfTF20UACaan9IwQU8AIQiYcTZyxXaK9j91QCnhXc27TXoPQ:
And it does:
{
"resource": {
"ref": {
"@ref": {
"id": "251411540589150720",
"class": { "@ref": { "id": "tokens" } }
}
},
"ts": 1576023579110000,
"instance": {
"@ref": {
"id": "251407645221585408",
"class": {
"@ref": {
"id": "users",
"class": { "@ref": { "id": "classes" } }
}
}
}
},
"hashed_secret": "$2a$05$l/WOFu6h9V3/vflDp6yGWOf/XDgCEJVG/G3JQmn6M9hzftYwivi0m"
}
}
Check credentials
You can verify whether a user’s credentials are valid, without creating
a token, by calling the Identify
function.
Let’s test whether the old and new credentials for our user are valid. Copy the following query, paste it into the Shell, and run it:
[
Identify(
Ref(Collection("users"), "251407645221585408"),
"secret password",
),
Identify(
Ref(Collection("users"), "251407645221585408"),
"new password",
),
]
When you run this query, the result should be:
[ false, true ]
Third-party delegation
Third-party delegation is the scenario where a third party uses our APIs to provide services to our users.
Using the authentication features of FaunaDB, we can provide unique tokens for each third-party client that allow the third party to access resources on behalf of our users, while providing a way for the user to revoke the third-party client’s access.
First, we create an index that allows us to list all of a user’s tokens.
Login
allows us to attach data to a token by adding extra fields.
We’ll use this capability to identify our tokens with the name of the
third-party service that will use the tokens. Copy the following query,
paste it into the Shell, and run it:
CreateIndex({
name: "tokens_by_instance",
permissions: { read: "public" },
source: Tokens(),
terms: [{ field: "instance" }],
values: [{field: ["data", "name"]}]
})
When you run this query, the result should be similar to:
{ ref: Index("tokens_by_instance"),
ts: 1576024400110000,
active: false,
serialized: true,
name: 'tokens_by_instance',
permissions: { read: 'public' },
source: Tokens(),
terms: [ { field: 'instance' } ],
values: [ { field: [ 'data', 'name' ] } ],
partitions: 1 }
Now we can create a token for each third-party service that our user uses. And we can do it all in a single query. Copy the following query, paste it into the Shell, and run it:
Map(
[
"Desktop App",
"Mobile App",
"Web Service"
],
Lambda(
"service",
Login(
Match(Index("users_by_email"), "alice@site.example"),
{
password: "new password",
data: { name: Var("service") }
}
)
)
)
When you run this query, the result should be similar to:
[ { ref: Ref(Tokens(), "251412696160797184"),
ts: 1576024681170000,
data: { name: 'Desktop App' },
instance: Ref(Collection("users"), "251407645221585408"),
secret: 'fnEDfTKD3rACAAN9IwrU8AIAa-IOXEqSP5rSkGjdQ_0eG9rBet0' },
{ ref: Ref(Tokens(), "251412696160799232"),
ts: 1576024681170000,
data: { name: 'Mobile App' },
instance: Ref(Collection("users"), "251407645221585408"),
secret: 'fnEDfTKD3rAKAAN9IwrU8AIA0su3H1YuSdUlgG5EQPRsqRcVyzQ' },
{ ref: Ref(Tokens(), "251412696160798208"),
ts: 1576024681170000,
data: { name: 'Web Service' },
instance: Ref(Collection("users"), "251407645221585408"),
secret: 'fnEDfTKD3rAGAAN9IwrU8AIAWe9UYsSvsOHgw0LHSnXj5CErYuo' } ]
Finally, in client application code, we can list all of the currently logged-in user’s tokens by querying the index that we built, when connecting to FaunaDB using the user’s token. The following code is written in JavaScript:
client.query(
q.Paginate(
q.Match(
q.Index("tokens_by_instance"),
q.Select("instance", q.Identity())
)
)
)
)
.then((ret) => console.log(ret))
.catch((err) => console.log("Error:", err))
When you execute this query in your client application code, after the user has logged in successfully, the output should be:
{ data: [ 'Desktop App', 'Mobile App', 'Web Service' ] }
Was this article helpful?
We're sorry to hear that.
Tell us how we can improve!
documentation@fauna.com
Thank you for your feedback!