When this API neatly integrates with your preferred identity provider, it becomes much easier and faster to secure applications with dynamic data.
We’ll first introduce Fauna, what concerns it removes, and explain how its API-first design fits into modern architectures. We’ll then explain how to set up an application and show how to secure Fauna data by directly taking advantage of Auth0 identities. When the database speaks JWT and can reason with the content, the possibilities are endless.
TL;DR: Fauna is a flexible, developer-friendly, transactional database delivered to as a secure, web-native API that eliminates the need for database provisioning, sharding, or replication. Integrating Auth0 with Fauna is easier than ever now, thanks to the recent third-party integration.
The Data Bump in the Road to Developer Nirvana
We live in an exciting era for building new applications because we have such great tooling and services available that make complex things ridiculously easy. Such tooling or services even comes with batteries included to immediately deliver a great user experience to our end users, allowing us to create applications that load fast, sport low latency page transitions, and implement a great user flow. For example, Auth0 allows us to set up secure authentication flows in minutes, while Netlify/Vercel enables us to deploy scalable applications with a single command.
When our apps become more complex, creating/storing/managing user data is often a crucial component of our application, and that is often where the developer experience suffers. We have to select a database by weeding through database jargon and complex fine print to understand the pros and cons so that we can take the caveats/limitations into account and build workarounds in our applications. We have to answer many questions:
- Where will I host my database?
- Will it scale, how big does my server need to be, how many do I need?
- Do I need to have servers in multiple regions to have low latency for all users?
- But how would I share data between multiple regions then?
- How will I protect my data?
- What is ACID or consistency?
How we answer these questions could be the determining factor in whether our application will succeed or fail. Some potential issues could be ignored initially, but with the risk that they come back to haunt us at a later stage. When they do come haunting us, it’s typically too late or very hard to start dealing with them. When it comes to dealing with dynamic data, the developer experience quickly goes back from Nirvana to something overly complex with insurmountable technical depth.
A Global Data API
What if we could connect our application to a data API that abstracts away these issues and gives us the functionality that answers those remaining questions for us, so we can focus on building application features? That is exactly what Fauna is!
In technical terms, Fauna is a globally distributed, scalable, and consistent OLTP cloud database that eliminates a wide variety of problems that can arise when using a database. The inner workings or terminology of a database might not be everyone’s cup of tea so let’s instead look at the developer experience.
Developers who use Fauna get a modern data API that behaves as expected delivers the low latency and automatic scaling required for a great end-user experience.
How Does Fauna Fit in My Application?
Fauna is built with modern applications in mind and therefore fits perfectly in traditional architectures as well as microservice or serverless architectures or the increasingly popular frontend-oriented Jamstack approach.
Unlike a traditional database, there are no open connections. That means there is also no overhead to create a connection or a connection limit, which makes it an ideal fit to be called from serverless functions. Unlike many NoSQL databases, there are no compromises on relations or consistency. Fauna is distributed and scales but maintains strong consistency and relations, which makes it a good fit to take over a traditional SQL workload where you would normally use Postgres.
A database that behaves like a stateless API with built-in security can be called from the frontend, which also makes it a perfect fit for the A of API in a Jamstack architecture or to speed up a Single Page Application (SPA). By avoiding that extra hop to a backend, we can significantly reduce latency. Distribute the data across regions, much like the CDN that delivers your static content but for dynamic data, minimizes the latency even more.
Users can interface with that Fauna API with either GraphQL or the extremely powerful Fauna Query Language (FQL). Just like GraphQL, FQL was developed to avoid over-fetching and easily retrieve all data or perform all manipulations in one single query. GraphQL excels at querying, whereas FQL excels at more advanced queries and complex transactional statements. You don’t have to choose; GraphQL and FQL go hand in hand, allowing you to use GraphQL where it feels easier and FQL where you are in need of raw power and flexibility.
When the database is a modern API, the client can take a more central role in application development. Such architectures where clients rely primarily on cloud services is what we call the client-serverless model.
What Is Client-Serverless?
Let’s take Auth0 and Fauna as an example to transform an existing architecture into a client-serverless model. Imagine a classical three-tier architecture where a client connects to a backend, which will then delegate data requests to the database. We call it three-tier since these tiers act like layers; a request typically always flows through the backend before accessing the database. When we combine this approach with Auth0, Auth0 will provide an access token that is sent to the server. The server then needs to interpret this access token and transform it into a secret to access the database and/or generate a query with the correct permissions.
Although Fauna is perfectly suited for a three-tier architecture, we can eliminate a few unnecessary complexities. Since Fauna API understands JSON Web Tokens (JWTs) and can be secured to allow access from the frontend, we could directly call Fauna from the client. Therefore, we don’t have to transform the token and/or generate a query to enforce our access permissions; instead, we can use the Auth0 access token directly as the secret to access the database.
This not only potentially eliminates an extra hop, but it also takes advantage of Fauna’s multi-region aspect without requiring you to set up a multi-region backend. Since Fauna’s security system can reason with the content of the JWT, you no longer need to write custom logic in your backend to secure your calls. Instead, you can write that logic as close to the data as it can possibly be, in the database!
In the diagram above, we just eliminated the backend, but does that mean you never need a backend? Definitely not; there are many reasons to pass certain calls through a (serverless) backend. However, it is no longer a requirement to pass each call through your backend if the database behaves like a secure API. Instead of a linear flow, the client becomes the center of the architecture, which communicates with cloud APIs such as Fauna and Auth0.
How Does the Fauna Integration Work?
In this article, we’ll focus on the Fauna side and assume that users have experience with Auth0 and know how to set up an Auth0 application and API and retrieve an Auth0 access token. If you are interested to see how the complete flow works, including the different Auth0 steps, there is a full tutorial, which can be found here.
1. Create a new Fauna database for Auth0
To start working with Fauna, we need an account. To create one, go to https://dashboard.fauna.com/ and sign up with Netlify/GitHub or use plain old credentials.
We can define how an Auth0 access token can access our data per database. Create a new database by clicking on the New Database button and filling in a name. Ensure the Prepopulate with demo data option is checked since we’ll use that data, and press Save.
We instantly get a new database presented.
The menu on the side provides us with everything we need:
- Collections: create collections and documents.
- Functions: create User Defined Functions (UDFs), which are similar to stored procedures.
- Shell: a dashboard shell to test out FQL queries.
- GraphQL: a GraphQL playground where we can upload a schema to create a GraphQL endpoint and test it out.
- Security: the star of this article, the security tab allows you to write access Roles that precisely define what data can be accessed by a given secret (and we’ll see that such a secret can be an Auth0 access token)
2. Accepting Auth0 JWT tokens
Fauna’s “Access Provider” allows you to specify that a database in your account should accept JWT tokens from an Identity Provider such as Auth0. To create one, go to the Security section of your newly created database, select PROVIDERS, and press the NEW ACCESS PROVIDER button.
You will need to fill in typical Identity Provider values such as issuer and JWKS URI, which in the case of Auth0 can be derived from your Auth0 domain:
- Issuer: https://< your auth0-domain >.auth0.com/
- JWKS URI: https://< your auth0-domain >.auth0.com/.well-known/jwks.json
The Auth0 domain itself can be found next to your profile when you log into the Auth0 dashboard.
Alternatively, you can grab the domain from your Auth0 application, as explained in the full tutorial.
With that information, fill in the Name, Issuer, and JSON Web Key Secret URI and press save (ignore the role for now).
3. Connecting to Fauna
Creating an Access Provider is all we need to accept JWT access tokens from Auth0, given that the audience (aud) field in these tokens is set correctly. The audience identifier can be found in the edit pane of the newly created Access Provider (see above). On the Auth0 side, you need to create an Auth0 API with this audience, which then allows you to include this API in the token (e.g., by configuring the client library). For the full instructions to correctly configure Auth0 tokens to include this audience, please refer to the full Fauna with Auth0 guide, which details each step of this process.
A valid Auth0 access token would look as follows when decoded:
{ "iss": "https://faunadb-auth0.auth0.com/", "sub": "google-oauth2|107696438605329289272", "aud": [ "https://faunadb-auth0.auth0.com/userinfo", "https://db.fauna.com/db/yxxeeaaqcydyy", ], "iat": 1602681059, "exp": 1602767459, "azp": "OgU7xmvv7pwumxlbilTA4MB7pErILWfS", "scope": "openid profile email", }
Once we have such a token, we can use it to query Fauna directly. We can either perform GraphQL queries (given that we uploaded a GraphQL schema or prepopulated the database with sample data upon creation) by passing the token along as the “Authorization Bearer” header.
Or start querying with FQL with one of the drivers (Android, C#, Go, Java, JavaScript, Python, Scala). The following snippet uses the JavaScript driver to retrieve all product references.
const faunadb = require('faunadb') const q = faunadb.query const client = new faunadb.Client({ secret: '<AUTH0 Token>' }) const { Documents, Collection, Paginate } = q client.query( Paginate(Documents(Collection(‘products’))) ).then((res) => { console.log(“Product references”, res) })
However, although we have a valid token, Fauna will not grant access to any resources until we define access roles. Luckily, Fauna roles allow you to reason with the information that is present on a third-party JWT, which brings us to the next topic: “Authorization.”
4. Authorizing Auth0 JWT tokens
As long as your Auth0 tokens are minted correctly, the newly created access provider ensures that the tokens are accepted. However, access without authorization is like an open door with a security guard to stop you from entering.
Let’s instruct the guard to let in anyone that has a valid Auth0 token meant for this database. To create a role, go back to the security section, select the ROLES tab, and click on the NEW CUSTOM ROLE button.
We can then provide fine-grained access in that role to specific collections (those were created by enabling demo data when we created the database) or a specific document in those collections. Let’s start by simply providing read access to products.
Once we save the role, we can go to the edit view of our provider.
And add the role.
Each Auth0 token from your domain that is directed towards this database will now have access to read products. Of course, that’s not very exciting; let’s instruct the guard to only allow a specific user to access the data.
5. Role-based authorization
A JWT token can contain a wealth of information, groups, scopes, permissions, custom attributes. All this information can be used to write Fauna security roles. Let’s start by using the value of the ‘sub’ attribute in a simple role, which by default is equal to the user’s ID.
Let’s go back to the definition of our role and click on the “code” icon to express the read access in a more flexible way with the Fauna Query Language (FQL). FQL is a procedural language that excels at expressing conditional queries making it a great fit for security roles.
Clicking the code button will unfold an editor in which we can write a simple or advanced FQL expression to secure our data.
Such an advanced FQL expression always starts with a Lambda (which is the equivalent of a function) that takes one or more parameters. In the case of reading access, the parameter is the reference of the document (in this case, the product) you are trying to access.
We’ll ignore the argument for now and instead write a hard-coded equality statement that grants access to a user with the id: “
google-oauth2|107696438605329289272
” which you can replace with one of your own Auth0 IDs. Lambda("ref", Equals( CurrentIdentity(), “google-oauth2|107696438605329289272” ) )
This is a rather simple example, which only grants read access to products to one user by verifying that the current user as returned by the CurrentIdentity() function equals a hard-coded Auth0 ID. CurrentIdentity() is the first function that allows you to reason with the contents of the JWT; when using this FQL function in a query, security role, or function, it will return the sub of the JWT that was used to call Fauna.
Although we used it in a role in the example above, we could similarly use CurrentIdentity() in a regular query to create a product that is owned by a specific user. For example, the following query will create a product and assign the owner of that product to the current identity. If we call that query with an Auth0 access token:
const client = new faunadb.Client({ secret: '<AUTH0 Token>' }) ... client.query( Create(Collection("products"), { data: { name: "My precious product", owner: CurrentIdentity() } } )
The result is a new product where the owner is the Auth0 identity.
Once we have products with an ‘owner’ field, we can change our role and say that only the owner of the product should get access to the product. It’s becoming slightly more interesting.
Lambda("ref", Equals( CurrentIdentity(), Select(["data", "owner"], Get(Var("ref"))) ) )
In this example, we have introduced a few FQL functions. Get() takes a reference and retrieves the document. We have previously mentioned that the parameter (“ref”) of the Lambda was the reference to the product we were trying to access. Retrieving the contents of that variable is done with Var(). Therefore, Get(Var(“ref”)) returns the product. We’ll select the “owner” attribute, which we had written when we created the document with the Select() function, and then verify whether this owner equals the CurrentIdentity() with the Equals() function.
Explaining the full capabilities of FQL is beyond the scope of this article, but in case you’re interested, the following cheatsheet provides an overview of the available functions, which should give you an idea of the powerful security roles you could write. If you’d like to dive in and start learning the language, this tutorial provides a great introduction to the language.
6. Advanced roles
The two roles we have written are still not the most exciting roles, but they do provide insights on the possibilities. Besides CurrentIdentity(), Fauna also provides CurrentToken(), which allows you to reason with the whole content of the JWT token and even allows you to conditionally set roles on the access provider depending on these values. If you want to know more, there is an extensive tutorial that goes into depth on the different approaches to secure your data with Auth0 and Fauna.
Auth0 provides you with full control of the content of the Auth0 token, while Fauna offers a security system driven by a powerful query language that allows you to specify what data can be accessed based on the contents of the JWT token precisely.
In essence, this means that you can virtually do anything! You could write your security role as simple or advanced as you want.
And the beautiful part? Security logic is often the bottleneck to an application, but in this case, you don’t have to think of scaling or distribution because the logic is part of the database and runs on the edge. This provides the best possible performance while taking full advantage of data consistency.
Conclusion
This article has illustrated how architecture can change by introducing client-serverless and where a global data API like Fauna fits in such an architecture. We also walked through step-by-step how the new integration for third-party providers makes this architecture easier to achieve and provided an overview of the different features and options that have become available to secure your Fauna database with Auth0 access tokens.
From a technological point of view, we live in an exciting time. New technologies are focused on Developer Experience and take the most cumbersome and hard to manage tasks out of our hands so that we can focus on building a great user experience. When two such technologies work together seamlessly, the way we build cloud applications can potentially change significantly.
If you want to know more about Fauna, take a look at www.fauna.com or join our community if you have further questions.
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.