React authentication using Auth0 and Hasura

React authentication using Auth0 and Hasura

In this article, we'll get to know the steps of authenticating a React application using Auth0 and Hasura, and we'll also build a Todo application using Apollo Client.

Let's start by configuring Auth0.

Configure Auth0

First head over to the auth0 dashboard and create a new tenant. How to create tenant

Give the tenant details and click Create. Tenant Create

Now, once your tenant is ready, go to Applications > Applications and then click Create Application.

Create Application

Then, give your application a name and choose Single Page Web Applications(because we're building in React). Now, click Create. Create SPA

Once your application is created, go to Settings and scroll down until you see Application URIs. Then add http://localhost:3000 to Allowed Callback URLs, Allowed Logout URLs, Allowed Web Origins, and Allowed Origins. Auth0 Application URIs Config

Scroll to the very bottom and hit Save Changes.

Now head over Applications > APIs and click Create API.

The JWT access token will be verified by this API.

Creating API

Now, give your API a name and also give it an identifier.

Later, we'll need to use the identifier name in our web application. If you give different name than mine, use that name when configuring in frontend.

API Identifiers

Now, one last configuration for Auth0.

In order for Hasura to work with JWT, the JWT must contain x-hasura-allowed-roles,x-hasura-default-role,x-hasura-user-id. And in order to add this to JWT that Auth0 will return on successfull login, we'll have to create a login action flow.

To create a login action flow.

Go to Actions > Flow and then click Login.

Login Action

Now, click on Custom and click Create Action. Custom Action Create

Now, give a name to a action and hit Create. Creating Auth0 Action

Add the following code inside onExecutePostLogin function and hit "deploy".

api.accessToken.setCustomClaim("https://hasura.io/jwt/claims", {
    "x-hasura-allowed-roles": ["user"],
    "x-hasura-default-role": "user",
    "x-hasura-user-id": event.user.user_id,
  })

Coding Action

Now, click on back to flow and then click on Custom and drag recently added Action and drop it in the middle of Start and Complete. Then click on Apply.

![Auth0 Action Flow] (cdn.hashnode.com/res/hashnode/image/upload/..)

Now, we're done with the Auth0 configuration. Later, we'll need Auth0 credentials, so, keep your Auth0 Dashboard open.

It's time to configure Hasura.

Configuring Hasura

Head over to Hasura Cloud, and create a new project if you haven't yet. Create Project

Now, click on Settings icon of the newly created project.

Click Settings

Once you're inside the Project Dashboard. Click on Env Vars. Then click on New Env Var.

click on Env Var

Now, in the key field search for jwt and choose HASURA_GRAPHQL_JWT_SECRET.

Chosee JWT SECRET

Now, go to this link to generate the JWT Secret.

Now, choose Auth0 as provider and add the auth0 domain from auth0 dashboard.

image.png

You can get the domain by heading to Auth0 Dashboard then click on Applications. Now click on Applications and then choose the recently created Single Page Application and then click on settings and you'll get the auth0 domain. image.png

Now, copy the generated JWT Config and head over the hasura and paste it in the value field of env var. Then click on Add.

And if you have used the self hosted Hasura, then add the aboce details as the environment varaible there.

image.png

Now that the configuration is done, let's create a test todo app now.

Creating a react application and configure Auth0

Now, let's start by creating a new React app.

Run the folowing command to create a new react app.

yarn create react-app hasura-auth0 
npx create-react-app hasura-auth0

Now, open the created application in an IDE, or code editor of your choice.

First, let's add all the required credentials in the .env file.

Create a .env file in the root of the application.

REACT_APP_AUTH0_DOMAIN=YOUR_SINGLE_PAGE_APPLICATION_DOMAIN
REACT_APP_AUTH0_CLIENTID=YOUR_SINGLE_PAGE_APPLICATION_CLIENTID
REACT_APP_AUTH0_AUDIENCE=YOUR_API_IDENTIFIER
REACT_APP_HASURA_API=HASURA_GRAPHQL_API

You can get Auth0 details from Auth0 Dashboard > Applications > Applications > Recently created App > Settings.

You can get Auth0 identifier from Auth0 Dashboard > Applications > APIs > Recently created API > Settings.

You can get Hasura Api from Hasura Dashboard > Project Settings.

image.png

At first let's add Auth0 authentication to our application.

For that first we need to add @auth0/auth0-react package to our application. Run following command to add.

yarn add @auth0/auth0-react
npm install @auth0/auth0-react --save

Now, wrap the <App/> component with Auth0Provider.

import { Auth0Provider } from "@auth0/auth0-react";
....

ReactDOM.render(
  <React.StrictMode>
    <Auth0Provider
      domain={process.env.REACT_APP_AUTH0_DOMAIN}
      clientId={process.env.REACT_APP_AUTH0_CLIENTID}
      audience={process.env.REACT_APP_AUTH0_AUDIENCE}
    >
      <App />
    </Auth0Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

Now, remove everything from the App.js file and add the following.

import {Header} from './Header';
import {Todos} from './Todos';
import {CreateTodo} from './CreateTodo';


function App () {
  return (
    <>
    <header>
      <Header />
    </header>
    <main>
      <Todos />
      <CreateTodo />
    </main>
    </>
  )
}

export default App;

Now, let's create Header, Todos, and CreateTodo components.

Create a new js file named Header.js inside src/ folder and add the following content.

import { useAuth0 } from "@auth0/auth0-react";

export function Header() {
  const { user, isAuthenticated, isLoading, loginWithPopup, logout } =
    useAuth0();
  if (isLoading) return <div>Loading...</div>;
  return (
    <>
      {isAuthenticated && (
        <div>
          <p>Welcome, {user.email}</p>
          <button onClick={() => logout()}>Logout</button>
        </div>
      )}

      {!isAuthenticated && (
        <div>
          <p>You're not logged in.</p>
          <button onClick={() => loginWithPopup()}>Log in</button>
        </div>
      )}
    </>
  );
}

Now, the authentication is done.

This component will display user email, when user is logged in. And will display login button if the user is not logged in.

Now, create a file named Todos.js and add the following.

export function Todos() {
  return (
    <>
      <h1>Todos</h1>
      <ul>
        <li>
          Todo Title
          <button>Delete</button>
        </li>
      </ul>
    </>
  );
}

Then, create a file named CreateTodo.js and add the following.

export function CreateTodo() {
  return (
    <>
      <form>
        <input type="text" placeholder="Todo Title" />
        <button type="submit">Create</button>
      </form>
    </>
  );
}

Now, let's connect our app to Hasura Graphql API. For that we'll use Apollo Client.

Add the following packages to your application.

yarn add @apollo/client graphql
npm install @apollo/client graphql --save

Now, create a new file named AuthorizedApolloProvider.js and add the following content.


import {
  ApolloClient,
  ApolloProvider,
  createHttpLink,
  InMemoryCache,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { useAuth0 } from "@auth0/auth0-react";

export function AuthorizedApolloProvider({ children }) {
  const { getAccessTokenSilently, isLoading, isAuthenticated } = useAuth0();
  if (isLoading) return <div>Loading...</div>;
  const httpLink = createHttpLink({
    uri: process.env.REACT_APP_HASURA_API,
  });

  const authLink = setContext(async () => {
    if (isAuthenticated) {
      const token = await getAccessTokenSilently();
      return {
        headers: {
          authorization: `Bearer ${token}`,
        },
      };
    } else {
      return {};
    }
  });
  const apolloClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
  });
  return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>;
}

Now, wrap the <App/> component in index.js with AuthorizedApolloProvider component.

import { AuthorizedApolloProvider } from "./AuthorizedApolloProvider";
...

ReactDOM.render(
  <React.StrictMode>
    <Auth0Provider
      domain={process.env.REACT_APP_AUTH0_DOMAIN}
      clientId={process.env.REACT_APP_AUTH0_CLIENTID}
      audience={process.env.REACT_APP_AUTH0_AUDIENCE}
    >
      <AuthorizedApolloProvider>
        <App />
      </AuthorizedApolloProvider>
    </Auth0Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

Database and permissions

Now, let's connect the database inHasura.

For this, head to Hasura cloud. Then click on Lunch Console, which is on the side of the project. image.png

Then click on Data and fill up the database details, if you already have a database else, click on Create Heroku Database to create a postgres db. image.png

I'll create a heroku database. So, once you click on Create Heroku Database. Click on Create Database and authenticate with your heroku account and database will be connected to Hasura.

image.png

Now, let's create a todos table in database to store todos.

For that, click on public and click on Create Table. image.png

Now, fill the fields as shown in the figure below.

image.png

Once, your table is created, click on Permissions to add the permission for the user.

In the Enter new role field type user. Here we're adding user, because while configuring login action flow, we set default role as 'user'. image.png Then, click on X icon under Insert section and now, we'll allow users to insert the todos without any checks and choose title and userId under Column insert permissions. Then hit Save Permissions.

image.png

Now, we'll allow user's to fetch their own todos only, for that we need to set select permission. So, under select section, click on X icon and then choose With custom checkand set userId equal to X-Hasura-User-Id. Then click toggle on under Column select permissions and hit permission.

image.png

In this application, we aren't working with update, so we're not setting the update permission, but if you want to allow user to update only those todos, which belongs to them only, then you need to choose With same custom checks as select under Pre-update check.

Now, since we're also deleting the todos, we'll allow user's to delete only thier own todos.

For that, under delete section, click on X icon and choose With same custom checks as select. And hit Save permissions.

Now, we're done with Database.

Building Todo

Let's fetch the todos, allow users to insert and delete the todos.

For this, first let's create a new file named queries.js where we'll store our graphql queries.

Paste the following content in the queries.js file.

import { gql } from "@apollo/client";
export const INSERT_TODO = gql`
  mutation InsertTodo($title: String = "", $userId: String = "") {
    insert_todos_one(object: { title: $title, userId: $userId }) {
      id
      title
      userId
    }
  }
`;

export const FETCH_TODOS = gql`
  query FetchTodos {
    todos {
      id
      title
      userId
    }
  }
`;

export const DELETE_TODOS = gql`
  mutation DeleteTodo($id: Int!) {
    delete_todos_by_pk(id: $id) {
      title
      id
      userId
    }
  }
`;

For creating the graphql queries, we can use Hasura Console. graphql-query.gif

Now, head to CreateTodo.js file to add the functionality to create a todo.

And add the following content.

import { useMutation } from "@apollo/client";
import { useAuth0 } from "@auth0/auth0-react";
import { useState } from "react";
import { FETCH_TODOS, INSERT_TODO } from "./queries";
export function CreateTodo() {
  const [title, setTitle] = useState("");
  const { user, isAuthenticated } = useAuth0();
  const [insertTodo, { loading, error }] = useMutation(INSERT_TODO);

  if (!isAuthenticated) return <></>;

  const handleSubmit = (e) => {
    e.preventDefault();
    insertTodo({
      //   auth0 stores userId in sub
      variables: { title: title, userId: user.sub },
      //   refresh the list of todos when the todo is inserted
      refetchQueries: [{ query: FETCH_TODOS }],
    }).then(() => {
      //  clear the input field
      setTitle("");
    });
  };
  return (
    <>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Todo Title"
        />
        {error && <pre>{JSON.stringify(error)}</pre>}
        <button type="submit">{loading ? "Creating" : "Create"}</button>
      </form>
    </>
  );
}

Now, try adding some todos, currently the todos, will not be visible, to check whether the todos are added or not, you can check it in Hasura Dashboard by going to Data > todos. image.png

Now, let's fetch the todos and also add functionalities to delete them too. For this, head to Todos.js file and add the following.

import { useMutation, useQuery } from "@apollo/client";
import { useAuth0 } from "@auth0/auth0-react";
import { DELETE_TODOS, FETCH_TODOS } from "./queries";
export function Todos() {
  const { data, loading, error } = useQuery(FETCH_TODOS);
  const { isAuthenticated } = useAuth0();
  const [deleteTodo] = useMutation(DELETE_TODOS);
  if (!isAuthenticated) return <></>;
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error Fetching Data: {JSON.stringify(error)}</p>;

  const onDelete = (id) => {
    //   delete the todo
    deleteTodo({
      variables: { id: id },
      //   refresh the list of todos when the todo is deleted
      refetchQueries: [{ query: FETCH_TODOS }],
    }).then(() => alert("Deleted"));
  };
  return (
    <>
      <h1>Todos</h1>
      <ul>
        {data.todos.map((todo) => (
          <li key={todo.id}>
            {todo.title}
            <button onClick={() => onDelete(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </>
  );
}

Now, our application is ready.

Let's see what we have built.