Adding Basic Authentication with Xata

Our to-do app is fully CRUD at this point since it can Create records in Xata, Read data from Xata, Update to-do items, and even Delete them. Now we will explore how we can add multiple users into our app and enable authentication for them.

If you're joining us in the middle, we covered creating data with Xata in the previous section. Maybe read that first before coming here for more context.

Add a New Table for Users

First we will add a new table for our users. The users table will contain columns username and password, both of string type. There are multiple ways to update the schema of our existing Xata database: it can be done from the Xata UI, within the VS Code Extension, or using the Xata CLI.

Here's how to update the schema conveniently with the Xata CLI, by running the following command:

# Edit the Xata schema
xata schema edit

In the dialog menu, let's choose "Add a table", set the Name of the table to users and optionally add the description A table of users. Once the table is created, choose the option Add a column below the users table to create two columns for it:

  • a column with the name username of type string and description A user's unique name.
  • a column with the name password of type string and description Always hash the password.

Once the columns are created, select "Run migration" and acknowledge the prompt requesting confirmation (Y/n).

Now also add a column to the items table, with the name user of type link linking to the users table. The link column type enables us to point a record in the items table to an associated record in the users table, thus representing a relation between those two records which live in different tables. This way, we can form one-to-many relations between items and users (as our target is that each item can be accessed by multiple users).

The Xata CLI regenerates the schema for the XataClient automatically once the migration is completed. In case you did not use the Xata CLI to create the users table and the link column, make sure to run xata codegen in order to regenerate your XataClient to include the latest schema updates.

Creating and Authenticating New Users

Let's add some Basic Auth to our Next.js App. Note that passwords should never be stored in plain text, so we will use bcrypt to handle cryptography for us, so that passwords are hashed before they are stored.

You can install bcrypt with the following commands:

# Install bcrypt module
npm i bcrypt
npm i @types/bcrypt -D

Now let's create our authentication utility:

// ./util/authorize.ts

import { IncomingMessage } from "http";
import { getXataClient } from "./xata";
import bcrypt from "bcrypt";
import { promisify } from "util";

const compare = promisify(bcrypt.compare);
const hash = promisify(bcrypt.hash);

type OurAuthResponse = { username?: string; isAuthenticated: boolean };

export const authorize = async (
  req: IncomingMessage
): Promise<OurAuthResponse> => {
  const { authorization } = req.headers;
  if (!authorization) {
    return { isAuthenticated: false };
  }

  // authorization: "Basic base64(username:password)"
  const [, credentials] = authorization.split(" ");
  const [username, password] = Buffer.from(credentials, "base64")
    .toString("utf-8")
    .split(":");

  const xata = getXataClient();
  const user = await xata.db.users.filter({ username }).getFirst();

  // user doesn't exist
  if (!user) {
    await xata.db.users.create({
      username,
      password: await hash(password, 10),
    });
    return { isAuthenticated: true, username };
  }

  // user exists, we have the password
  const passwordsMatch = compare(password, user.password);

  if (!passwordsMatch) {
    return { isAuthenticated: false, username };
  }

  return { isAuthenticated: true, username };
};

Time to configure our application to use the authentication utility by modifying getServerSideProps to call authorize:

// ./pages/index.tsx

import Head from "next/head";
import { FC } from "react";
import { AddTodoForm } from "../components/AddTodoForm";
import { authorize } from "../util/authorize";
import { getXataClient } from "../util/xata";

type Props = Awaited<ReturnType<typeof getServerSideProps>>["props"];

const Index: FC<Props> = ({ todos }) => {
  return (
    <main>
      <Head>
        <link rel="stylesheet" href="https://unpkg.com/mvp.css" />
      </Head>
      <h1>My Todo List</h1>
      <AddTodoForm />
      <ul>
        {todos.map((t) => (
          <li
            key={t.id}
            style={{ display: "flex", gap: 8, alignItems: "center" }}
          >
            <>
              <label>
                <input
                  onChange={() => {
                    fetch("/api/do-todo", {
                      method: "POST",
                      headers: {
                        "Content-Type": "application/json",
                      },
                      body: JSON.stringify({
                        id: t.id,
                        is_done: !t.is_done,
                      }),
                    }).then(() => window.location.reload());
                  }}
                  checked={t.is_done}
                  type="checkbox"
                />
                {t.label}
              </label>
              <button
                onClick={() => {
                  fetch("/api/delete-todo", {
                    method: "POST",
                    headers: {
                      "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                      id: t.id,
                    }),
                  }).then(() => window.location.reload());
                }}
              >
                Delete
              </button>
            </>
          </li>
        ))}
      </ul>
    </main>
  );
};

export default Index;

export const getServerSideProps = async ({ req, res }) => {
+  const { isAuthenticated, username } = await authorize(req);
+
+  if (isAuthenticated) {
+    const xata = getXataClient();
+    const todos = await xata.db.items
+      .filter("user.username", username) // to-do items are now filtered to the current authenticated user
+      .getMany();
+
    return {
      props: {
        todos,
      },
    };
+  } else {
+    res.writeHead(401, {
+      "WWW-Authenticate": "Basic realm='This is a private to-do list'",
+    });
+    return { redirect: { destination: "/", permanent: false } };
+  }
};

Updating all API routes to use Authentication

Finally let's update all our application's API routes to use the new authentication workflow:

// ./pages/api/add-todo.ts

import { NextApiHandler } from "next";
import { authorize } from "../../util/authorize";
import { getXataClient } from "../../util/xata";

const handler: NextApiHandler = async (req, res) => {
+    const { isAuthenticated, username } = await authorize(req);
+    if (!isAuthenticated) {
+        res.status(401).end()
+        return;
+    }

+    const { label, is_done } = req.body;
     const xata = getXataClient();
+    const user = await xata.db.users.filter({ username }).getFirst();
-    await xata.db.items.update({ is_done, label });
+    await xata.db.items.create({ label, user: { id: user.id } });
     res.end()
}

export default handler;
// ./pages/api/do-todo.ts

import { NextApiHandler } from "next";
import { authorize } from "../../util/authorize";
import { getXataClient } from "../../util/xata";


const handler: NextApiHandler = async (req, res) => {
+    const { isAuthenticated } = await authorize(req);
+    if (!isAuthenticated) {
+        res.status(401).end()
+        return;
+    }

    const { id, is_done } = req.body;
    const xata = getXataClient();
    await xata.db.items.update({ id, is_done });
    res.end()
}

export default handler;
// ./pages/api/delete-todo.ts

import { NextApiHandler } from "next";
import { authorize } from "../../util/authorize";
import { getXataClient } from "../../util/xata";

const handler: NextApiHandler = async (req, res) => {
+    const { isAuthenticated, username } = await authorize(req);
+    if (!isAuthenticated) {
+        res.status(401).end()
+        return;
+    }

    const { id } = req.body;
    await getXataClient().db.items.delete(id);
    res.end()
}

export default handler;

Conclusion

We've made it! Now our Next.js app is authenticating users, storing their password in a secure cryptographic way in Xata and automatically relates items to our authenticated users! We finally have a fully CRUD application with authentication, built on Xata.

This todo application is open source, and all of its code is available on GitHub. If you'd like to run it locally, you'll need to add a Xata API key in an .env file at its root after you clone it.

Don't forget—we're always available to chat if you'd like to on our Discord! If you'd like to request a new feature, you can do so on our feature board. We're also ready to support you with real problems if you need some support. Building something cool? Tell us on Twitter!

Happy hacking!


Last modified 28 days ago