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.

#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 { createClient } from "nhost-js-sdk";

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

const { auth, storage } = createClient(config);

export { auth, storage };

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

#Add NhostAuthProvider

Now it's time to wrap our App with another provider. The NhostAuthProvider. This provider indicates if a user is logged in or not in the AuthGate component that we'll implement soon.

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

$ npm install @nhost/react-auth@v1.0.5

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. Finally we wrapped our app with NhostAuthProvider, passing auth as a prop so that the same auth object gets used in both Nhost providers.

Now, let's create the login component.

#Login

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

Create a directory src/components and 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 log in.

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 { gql, useSubscription, useMutation } from "@apollo/client";
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 log in, we see No data. And if we log out again, our todos are back. What's going on?

When logged out, the public role was used, which still had permissions to select todos. When we log in, the user role was used instead, which doesn't have permission to select todos. Yet!

Things we need to do 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 can 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 todos.

#Auth Gate

Let's now create the AuthGate component that ensures only authenticated users can access specific 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 checks whether a user is logged in and redirects to /login otherwise.

In src/index.js we add the AuthGate component 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 "utils/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. Before we do that all existing todos must be deleted. To delete all existing todos, click Browse Rows for the todos table, select all todos and click on the trash can icon.

That deleted all todos. Now let's add the new user_id column.

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

user_id refers 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 need to create a Foreign Key.

A Foreign key is a link between two tables

The foreign key links the users table to the todos table.

You create the foreign key in the same Modify tab. Scroll down a bit to Foreign Keys and click Add. See the image below of 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 the image below.

Remove public permissions from Hasura

Now we'll add permissions for the user role.

All logged-in users have the user role.

Insert

First, we'll 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 the image below.

User insert permission

Select

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

User select permission

The app is completed

You have probably already tested your app during this guide, but you have finished you're app. Congratulations!