Authentication

Currently, everyone can select and insert todos. That is ok to start with but now it's time to secure our API by adding authentication.

Authentication is built-in to Nhost and easy to get started with.

#Create User

Let's first create our user by going to the project's Auth section.

Add user

You will use this user to access your todos later on. But first, we will install nhost-js-sdk to help us with authentication.

#Install and Configure nhost-js-sdk

nhost-js-sdk is an open-source JavaScript library for interacting with Nhost's auth and storage.

$ npm install nhost-js-sdk

Now, we need to configure and initialize a connection to Nhost. Create a new folder, src/utils, and create nhost.js inside it.

src/utils/nhost.js
import nhost from "nhost-js-sdk";

const config = {
  base_url: "https://backend-REPLACE.nhost.app",
};

nhost.initializeApp(config);

const auth = nhost.auth();
const storage = nhost.storage();

export { auth, storage };

Please don't forget to replace https://backend-REPLACE.nhost.app with your own endpoint.

#Add NhostAuthProvider

Now it's time to wrap our App with another provider. The NhostAuthProvider is used to know whether a user is logged in or not.

First let's install the @nhost/react-auth package:

$ npm install @nhost/react-auth

We'll also pass our auth object as a prop to both providers, NhostAuthProvider and NhostApolloProvider, so that the whole app can share the same auth state.

src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { NhostApolloProvider } from "@nhost/react-apollo";
import { NhostAuthProvider } from "@nhost/react-auth";
import { auth } from "utils/nhost";
ReactDOM.render( <React.StrictMode>
<NhostAuthProvider auth={auth}>
<NhostApolloProvider auth={auth} gqlEndpoint="https://hasura-REPLACE.nhost.app/v1/graphql" >
<App /> </NhostApolloProvider>
</NhostAuthProvider>
</React.StrictMode>, document.getElementById("root") );

Now it's a good time to recap what has been done so far in this chapter. We started by creating a new user for our Nhost project, followed by installing and configuring nhost-js-sdk, and finally we wrapped our app with NhostAuthProvider, passing auth as a prop, so that the same auth object is used everywhere.

Next, we will create the login component.

#Login

We need to write a login component that handles the form submittion of a user's data, and also some scaffolding for the app routes.

Create the Login component with the following content:

src/components/login.js
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { auth } from "utils/nhost";

export function Login(props) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const history = useHistory();

  async function handleSubmit(e) {
    e.preventDefault();

    // login
    try {
      await auth.login(email, password);
    } catch (error) {
      alert("error logging in");
      console.error(error);
      return;
    }

    // redirect back to `/`
    history.push("/");
  }

  return (
    <div>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        <input
          type="password"
          placeholder="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <button>Login</button>
      </form>
    </div>
  );
}

Now let's create two routes. One for / where we'll have our todos listed, and one for /login where users can login.

src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Login } from "components/login";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { NhostApolloProvider } from "@nhost/react-apollo"; import { NhostAuthProvider } from "@nhost/react-auth"; import { auth } from "utils/nhost"; ReactDOM.render( <React.StrictMode> <NhostAuthProvider auth={auth}> <NhostApolloProvider auth={auth} gqlEndpoint="https://hasura-REPLACE.nhost.app/v1/graphql" >
<Router>
<Switch>
<Route exact path="/login">
<Login />
</Route>
<Route exact path="/">
<App />
</Route>
</Switch>
</Router>
</NhostApolloProvider> </NhostAuthProvider> </React.StrictMode>, document.getElementById("root") );

Finally, add two links to App.js. One to /login and one that will logout the user.

src/App.js
import React, { useState } from "react";
import { useSubscription, useMutation } from "@apollo/client";
import gql from "graphql-tag";
import { Link } from "react-router-dom";
import { auth } from "utils/nhost";
const GET_TODOS = gql` subscription { todos { id created_at name completed } } `; const INSERT_TODO = gql` mutation($todo: todos_insert_input!) { insert_todos(objects: [$todo]) { affected_rows } } `; function App() { const { data, loading } = useSubscription(GET_TODOS); const [insertTodo] = useMutation(INSERT_TODO); const [todoName, setTodoName] = useState(""); if (loading) { return <div>Loading</div>; } return ( <div>
<Link to="/login">Login</Link>
<div onClick={() => auth.logout()}>Logout</div>
<div> <form onSubmit={async (e) => { [...] </form> </div> {!data ? ( "no data" ) : ( <ul> {data.todos.map((todo) => { return <li key={todo.id}>{todo.name}</li>; })} </ul> )} </div> ); } export default App;

Phew, that's a lot of code. Let's stop here and recap what we have.

Both routes (/ and /login) are still unprotected, we'll fix that soon. When we visit / we still see all todos, that's fine for now since we allow the public role to select them.

On the other hand, when we login, we only see No data. And if we logout again our todos are back. What's going on?

When logged out, the public role is used, which still has permissions to select todos. When we are logged in, the user role is used instead, which doesn't have permission to select todos, yet!

These are the things we still have to do in order to secure our app:

  • Protect the / route so that it is available to logged in users only.
  • Remove all permissions for the public role. Only logged in users are able insert and select todos.
  • Add user_id to the todos table so that every todo belongs to a user.
  • Set permissions so that users are only allowed to insert and select their own todos.

#Auth Gate

Let's now create the AuthGate component that will make sure users are authenticated before they can access certain routes.

src/components/auth-gate.js
import { Redirect } from "react-router-dom";
import { useAuth } from "@nhost/react-auth";

export function AuthGate({ children }) {
  const { signedIn } = useAuth();

  if (signedIn === null) {
    return <div>Loading...</div>;
  }

  if (!signedIn) {
    return <Redirect to="/login" />;
  }

  return children;
}

AuthGate simply checks whether a user is logged in and redirects to /login otherwise.

Open src/index.js and add the AuthGate around the routes we want only logged in users to access.

src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Login } from "components/login";
import { AuthGate } from "components/auth-gate";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { NhostApolloProvider } from "@nhost/react-apollo"; import { NhostAuthProvider } from "@nhost/react-auth"; import { auth } from "util/nhost"; ReactDOM.render( <React.StrictMode> <NhostAuthProvider auth={auth}> <NhostApolloProvider auth={auth} gqlEndpoint="https://hasura-xxx.nhost.app/v1/graphql" > <Router> <Switch> <Route exact path="/login"> <Login /> </Route>
<AuthGate>
<Route exact path="/"> <App /> </Route>
</AuthGate>
</Switch> </Router> </NhostApolloProvider> </NhostAuthProvider> </React.StrictMode>, document.getElementById("root") );

Now users can only access / if they are logged in.

#Connect todos to users

A todo should belong to a user and only be visible to that user. We will now add a new column to our todos table but first we must delete all todos. Click Browse Rows for the todos table, select all todos and click on the trash can icon.

All todos are now deleted.

Click on Modify and then click the button Add a new column. Name the new column user_id with the type UUID. Also make sure to uncheck Nullable. Click Save.

user_id will refer to the id of the user the todo belongs to.

New user_id column

For our GraphQL API, and database, to understand that the user_id refers to the user's id we will create a Foreign Key.

A Foreign key is a link between two tables

Our foreign key will act as a link between the users table and the todos table.

You create the foreign key in the same Modify tab. Scroll down a bit to Foreign Keys and click Add. See image bellow how to set the Foreign Key and click Save.

Foreign key

For Hasura to use the foreign key relation in the GraphQL API we need to track the foreign key relationship. Go to the DATA tab and click Track all.

Track all relationships

#Permissions for users

Remove permissions for the public role

We won't use the public role anymore so let's remove all permission for that role. See image bellow.

Remove public permissions from Hasura

Now we'll add permissions for the user role.

All logged in users have the user role.

Insert

First set the Insert permission.

A user can only insert name because all other columns will be set automatically. More specifically, user_id will be set to the id of the user making the request (x-hasura-user-id) and is configured in the Column presets section. See image bellow.

User insert permission

Select

For Select permission, set a custom check so users are only allowed to select todos where user_id is the same as their own user id. In other words: users are only allowed to select their own todos. See image bellw.

User select permission

We are done!

You have probably already tested your app during this guide, but your app is now finished. Congratulations!