A tutorial for using Firebase to add authentication and authorization to a realtime Hasura app

This tutorial was written by Junyu Zhan and published as part of  the Hasura Technical Writer Program - an initiative that supports authors who write guides and tutorials for the open source Hasura GraphQL Engine.

Introduction

Hasura helps you build production-level 3factor apps really fast. Read more about building 3factor apps with event-driven programming. In addition to generating CRUD GraphQL APIs from your PostgreSQL database, it also provides ways to authenticate users using webhooks or JWT and helps you define granular access controls rules for your GraphQL schema (authorization).  However, integrating an auth system with Hasura backend and some  frontend still requires some effort, and can be tricky.  This tutorial aims to demonstrate how to do that by making a real-time voting app like this sample app, but with auth built in. We are going to use Hasura for the backend, Firebase Authentication for authentication and React for the frontend.

It has mainly 3 steps:

  1. Set up Hasura and create the data model using the Hasura Console
  2. Set up Authentication
  3. Build the React web app

Prerequisites

  • React
  • GraphQL
  • SQL

Using Firebase Authentication versus building your own

Building a robust authentication system is no small effort. It's so important that it can make or break your app. Because the main focus of  this tutorial is to integrate an auth system with Hasura and React, we are going to use the off-the-shelf Firebase Authentication. It's secure, offers many useful features such as third-party and passwordless sign-in and has a generous free tier.

Step One: Hasura

The quickest and easiest way to use Hasura is via the Hasura Cloud. Follow the "Quickstart with Hasura Cloud" guide to get a Hasura instance up and running.

After creating the Hasura instance, we need to set some environment variables in the project settings.

Since we will use  JWT from Firebase, set HASURA_GRAPHQL_JWT_SECRET to:

{
    "type":"RS256",
    "jwk_url": "https://www.googleapis.com/service_accounts/v1/jwk/[email protected]",
    "audience": "<firebase-project-id>",
    "issuer": "https://securetoken.google.com/<firebase-project-id>"
}

Note: Firebase shares JWKs between multiple tenants. They use the aud claim of JWT to specify the intended audience for the JWT. Setting the audience field in the Hasura JWT configuration will make sure that the audclaim from the JWT is also checked during verification. Not doing this check will allow JWTs issued for other tenants to be valid as well. In these cases, you MUST set the audience field to the appropriate value. Failing to do so is a major security vulnerability.

Finally, set HASURA_GRAPHQL_UNAUTHORIZED_ROLE to anonymous because we do allow unauthenticated users to write and read some data.

Now it's time for data modeling. First, we need a "programming_language" table with "name" and "vote_count" fields.

Programming language table

Also, we need a "loved_language" table to record whether a language is loved by some users. Since a user can only love a language once, we need to set the primary key as name and user_id combined. There is no way to do that in the "Add Table" UI, but Hasura conveniently provides a  way to execute raw SQL:

Loved language table

After you create these two tables, Hasura will notice the one-to-many relationship between them and help you create the corresponding GraphQL relationship.

Relationships

Hooray! Now that we have a data model, you can play with the API in GraphiQL. Insert some of your favorite languages. Give them some votes. Love them as a random "user_id". Since we are signed in as admin, we can do anything we want. But we need to set proper permissions for the  "anonymous" and "user" roles. We allow both of them to select and update "programming_language":

Programming language permission

For "loved_language", we only allow the "user" role to insert, select and delete. Notice that for the "insert" permissions, the "user_id" must come from "X-Hasura-User-Id".

Loved language permission

With permissions set, all we need is a secure way to get "X-Hasura-User-Id".

Step Two: Firebase Auth

Go to the Firebase website to create a new project. By default it's on the free plan, so don't worry about charges.

In the Authentication section of the Firebase console, turn on the Google sign-in provider. In this tutorial, we only use Google sign-in, but adding other providers is trivial. Notice at the bottom of the page, in "Authorized domains", localhost and a Firebase domain are automatically added. If you later decide to deploy the React app to another domain, you need to add it here for Google sign-in to work.

Firebase Authentication

Now we can sign in users and get their id token for Hasura in the React app, using the Firebase JS SDK. But for Hasura to know the identity of these users, whose data are stored in Firebase, we need to  add some specific "custom claims" required by Hasura to the token. We will use Cloud Functions for Firebase to do that, following the example in the Hasura repo.

A cloud function is, well, some function that automatically runs "in  response to events triggered by Firebase features and HTTPS requests". In our case the event is firebase user creation. When that happens, we'd  like to add some extra data to the user's id token. The code is straightforward:

The code with comments needs some more explanation. When a user signs in our React app, Firebase immediately gives us that user's data, including the id token we need to send to Hasura. But if it's the first time, in which the user is just created, the token may not include those custom claims. That's the reason we use Firebase real-time database to listen to a token refresh.

Deploying a cloud function is simple. Follow the official Get started guide, replace index.js with the code above and run firebase deploy --only functions. That's it.

Before you leave the Firebase console, you need to change one more setting. Go to the Realtime Database Rules section and update the rules. We need to define conditions for when data can be read by users.

{
  "rules": {
    "metadata": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid"
      }
    }
  }
}

You can read more about Firebase Database Rules here to understand different read and write conditions that can be specified.

Step Three: React

Finally, it's time for us to build the exciting UI. We will use Apollo Client to query our GraphQL API. Follow the client setup guide to add all the needed npm packages.

Since the app is simple, we are only going to build two components:  "Auth" and "App". "Auth" uses Firebase SDK to sign in users and pass  that state to "App". "App" includes all the business logic: subscribing to the real-time data, voting and loving languages.

Auth:

The code is straight forward if you are familiar with the new Hooks API.  Notice how we use the Firebase realtime database to listen to the refreshTime which we set up in the Firebase cloud function. Also, we check whether a user's id token already contains custom claims to avoid useless refreshing.

App:

Notice how Hasura and GraphQL enable us to flexibly query data we need based on different auth state.

Wrapping up

In this tutorial, we have built a real-time voting app with Hasura. We integrated a robust auth system both on the backend and frontend. You can see how Hasura makes tedious and hard jobs easy by providing a pluggable GraphQL and auth interface. Based on this model, there really is no limit on what kind of amazing apps you can build.

All the code in this tutorial are in the Github repo.

About the author

Junyu Zhan is a full-stack web developer living in China. Right now he is building a cross-platform App with Flutter, React and Hasura.  He loves taking long walks and swimming in his spare time. You can contact him at his email address ([email protected]) or Twitter Account (https://twitter.com/thezjy1).

Blog
07 Sep, 2022
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.