Secure Your Remix.js App: A Guide to Authentication and Authorization
Tags: react, tutorial, node, typescript
Published: 2023-05-14 15:44:35

## What you will learn

I hope that after reading this article you will have enough foundations to add any authentication and authorization strategy in Remix applications.

![final result](https://res.cloudinary.com/dj5iihhqv/image/upload/v1683668369/2023-05-06_at_22.54.52_-_Purple_Finch_emgw3f.gif)

In addition, we will also create domain functions, database schema definition, form handling, among other topics.

## What does this article cover

We will cover several aspects, including:

- Tailwind Configuration
- Form validation
- Database schema modeling
- Authentication and authorization

## Prerequisites

Before starting the article, it is recommended that you have knowledge of React, Remix, Tailwind and a prior knowledge of nested routes and ORM's.

## Creating the Project

To initialize a project in Remix we execute the following command:

```sh
npx create-remix@latest remizzle
cd remizzle
```

We start the dev server with the following command:

```sh
npm run dev
```

We are going to use the `Just the basics` type, with a deployment target of `Remix App Server` and we are going to use `TypeScript` in the application.

## Set up Tailwind 

Next, let's configure Tailwind in our project, starting with the installation:

```sj
npm install -D tailwindcss
npx tailwindcss init --ts
```

Then we add the following in the `remix.config.js` file:

```js
module.exports = {
  future: {
    tailwind: true,
  },
};
```

We also make the following changes to `tailwind.config.ts`:

```ts
import type { Config } from "tailwindcss";

export default {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],
  // ...
} satisfies Config
```

Next, we create a file called `tailwind.css` inside the `app/` folder with the following:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
Now let's import the newly created css file in `root.tsx`:

```tsx
import { cssBundleHref } from "@remix-run/css-bundle";
// ...

import stylesheet from "~/styles/tailwind.css";

export const links: LinksFunction = () => [
  ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
  { rel: "stylesheet", href: stylesheet },
];

// ...
```

To make app development easier, let's take advantage of the pre-styled components of the Flowbite library, starting by installing the dependency:

```sh
npm install flowbite
```

Going back to `tailwind.config.ts` we add the flowbite plugin in the configuration:

```ts
import type { Config } from "tailwindcss";

export default {
  // ...
  plugins: [require("flowbite/plugin")],
} satisfies Config;
```

## Set up Routes Gen

First of all, let's define each of our application's routes, taking into account that theoretically we'll have two layouts, one for the authentication pages and another for the protected routes.

Another very important point is that the app's root route (`/`) must redirect the user taking into account their status, if they are logged in they go to protected routes, otherwise they go to authentication.

Now that we have a small overview, we can define the following routes:

- `_index.tsx` - app main route
- `auth.tsx` - authentication page(s) layout
- `auth.sign-in.tsx` - page where we are going to log in
- `auth.sign-up.tsx` - page where we will create a new account
- `protected.tsx` - protected page(s) layout, and we are going to add protection to ensure that you can only access it if you have a session
- `protected._index.tsx` - main route of the protected pages and in this article it is where we will see the user information and we will log out

With the above routes created inside the `routes/` folder, we can install the following dependencies:

```sh
npm install routes-gen @routes-gen/remix --dev 
```

And in the `package.json` file we add the following script:

```js
{
  // ...
  "scripts": {
    // ...
    "routes:gen": "routes-gen -d @routes-gen/remix"
  },
  // ...
}
```

When we run the command, it will create the `routes.d.ts` file inside the `app/` folder with the data type declarations taking into account the application's routes. Remembering that the command is this:

```sh
npm run routes:gen
```

## Set up Drizzle ORM

To make this article accessible to more people, we are going to use the SQLite database and install the following dependencies:

```sh
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
```

The next step will be to create the `config.server.ts` file in a folder called `db/` inside the `app/` folder, with the following:

```ts
// @/app/db/config.server.ts
import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import Database from "better-sqlite3";

const sqlite = new Database("sqlite.db");

export const db = drizzle(sqlite);

migrate(db, { migrationsFolder: "./app/db/migrations" });
```

As you may have noticed in the code block above, we start by defining the database, then we use the SQLite driver and export its instance (`db`).

Shortly afterwards we used the `migrator` function to carry out automatic migrations in our database taking into account what is generated inside the `app/db/migrations/` folder.

Then we define our database tables in the `schema.server.ts` file, also inside the `db/` folder, taking advantage of drizzle primitives and exporting the schema that was created:

```ts
// @/app/db/schema.server.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: integer("id").primaryKey(),
  username: text("username").notNull(),
  email: text("email").notNull(),
  password: text("password").notNull(),
  createdAt: integer("created_at", { mode: "timestamp" }),
});
```

The next step is to add the script responsible for generating the database migrations to the `package.json` file:

```js
{
  // ...
  "scripts": {
    // ...
    "db:migrations": "drizzle-kit generate:sqlite --out ./app/db/migrations --schema ./app/db/schema.server.ts"
  },
  // ...
}
```

Running the above command will generate migrations inside the `app/db/migrations` folder that can be applied to the database. With this we can go to the next section.

## Domain Functions

In this section of the article we will define the domain functions responsible for registering a new user, as well as starting a new session.

The first step will be to add the following dependencies:

```sh
npm install zod domain-functions argon2
```

Next, inside the `app/` folder, we'll create a folder called `common/` and inside this an `authSchema.ts` file. This file will specify some schemas that will be used in form validation and domain functions.

```ts
// @/app/common/authSchema.ts
import { z } from "zod";

export const authSchema = z.object({
  username: z
    .string()
    .min(3, "Must contain at least 3 chars")
    .max(24, "Must contain 24 chars max"),
  email: z.string().email(),
  password: z
    .string()
    .min(6, "Must contain at least 6 chars")
    .max(12, "Must contain 12 chars max"),
});

export type RegisterAccountAuth = z.infer<typeof authSchema>;

export const authSchemaWithoutUsername = authSchema.omit({ username: true });

export type GetUserByEmailAuth = z.infer<typeof authSchemaWithoutUsername>;
```

With the schemas created, still inside the `app/` folder, let's create a new `domain/` folder with an `account.server.ts` file. Inside this file we will import the instance of our database, the database schema and the authentication schemas.

Then we create the `createAccount` function, which will check if there is an account on the platform with the same credentials, if not, we will hash the password and save the user in the database.

Another function that we are going to create is `getAccountByEmail`, which will check if the user exists, if it exists, we will check if the password is correct and if it is, then the credentials are valid.

```ts
// @/app/domain/account.server.ts
import { InputError, makeDomainFunction } from "domain-functions";
import type { InferModel } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { hash, verify } from "argon2";

import { authSchema, authSchemaWithoutUsername } from "~/common/authSchema";
import { db } from "~/db/config.server";
import { users } from "~/db/schema.server";

export const createAccount = makeDomainFunction(authSchema)(async (data) => {
  const result = db
    .select()
    .from(users)
    .where(eq(users.email, data.email))
    .get();

  if (result) {
    throw new InputError("Email already taken", "email");
  }

  const { password, ...rest } = data;

  const hashedPassword = await hash(password);

  const newUser: InferModel<typeof users, "insert"> = {
    ...rest,
    password: hashedPassword,
    createdAt: new Date(),
  };

  const record = db.insert(users).values(newUser).returning().get();

  if (!record || !record.id) {
    throw new Error("Unable to register a new user");
  }

  return record;
});

export const getAccountByEmail = makeDomainFunction(authSchemaWithoutUsername)(
  async (data) => {
    const result = db
      .select()
      .from(users)
      .where(eq(users.email, data.email))
      .get();

    if (!result || !result.email) {
      throw new InputError("Email does not exist", "email");
    }

    const isValidPassword = await verify(result.password, data.password);

    if (!isValidPassword) {
      throw new InputError("Password is not valid", "password");
    }

    return result;
  }
);
```

With the domain functions completed, we can move on to the next point.

## Reusable components

Before moving directly to validating the forms, we need to create the components that we are going to reuse in them, I am talking about the `Input` component and the `Button`.

First we need to install the following dependencies:

```sh
npm install remix-validated-form @remix-validated-form/with-zod
```

Starting by creating the `Input` component, we can do the following:

```tsx
// @/app/components/Input.ts
import type { InputHTMLAttributes } from "react";
import { useField } from "remix-validated-form";

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  name: string;
  label: string;
}

export function Input({ name, label, ...rest }: InputProps) {
  const { error, getInputProps } = useField(name);
  return (
    <div className="space-y-2">
      <label htmlFor={name}>{label}</label>

      <input
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
        {...getInputProps({ id: name })}
        {...rest}
      />

      {error && (
        <p className="text-sm text-red-600 dark:text-red-500">{error}</p>
      )}
    </div>
  );
}
```

Now we can create the `Button` component, in this case we will not take into account the submission state, but this can be added in the future.

```tsx
// @/app/components/Button.ts
import type { ButtonHTMLAttributes } from "react";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
}

export function Button({ label, ...props }: ButtonProps) {
  return (
    <button
      className="text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
      {...props}
    >
      {label}
    </button>
  );
}
```

With all the components created, we can move on to the next point.

## Authenticator setup

In this article we are not going to use any third-party service, we are going to build the application system ourselves but to facilitate the whole process we are going to use the [Remix Auth](https://github.com/sergiodxa/remix-auth) project. The strategy that will be used in this article is the [FormStrategy](https://github.com/sergiodxa/remix-auth-form), but there are several strategies created by the community, accessing [this](https://github.com/sergiodxa/remix-auth/discussions/111) link.

The first step is to install the following dependencies:

```sh
npm install remix-auth remix-auth-form
```

The next step is to create the storage where we are going to save our session, for that we are going to take advantage of the `createCookieSessionStorage` remix primitive and we are going to add the following configuration in the `storage.server.ts` file in the `auth/` folder:

```ts
// @/app/auth/storage.server.ts
import { createCookieSessionStorage } from "@remix-run/node";

export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "_session",
    sameSite: "lax",
    path: "/",
    httpOnly: true,
    secrets: ["super-secret"],
    secure: process.env.NODE_ENV === "production",
  },
});

export const { getSession, commitSession, destroySession } = sessionStorage;
```

With the storage session created, we can now configure our strategy, starting by importing the storage solution that was created, as well as the domain function responsible for logging in the user. 

When defining the authentication strategy, we check if the context has the formData from the login form, if it does, we'll get the email and the password that we'll pass later on in the `getAccountByEmail` function arguments. If the account exists and the credentials are correct, we will get the username and user identifier to add to the session.

```ts
// @/app/auth/authenticator.server.ts
import { Authenticator } from "remix-auth";
import { FormStrategy } from "remix-auth-form";

import { sessionStorage } from "~/auth/storage.server";
import { getAccountByEmail } from "~/domain/account.server";

interface User {
  userId: number;
  username: string;
}

export const EMAIL_PASSWORD_STRATEGY = "email-password-strategy";

export const authenticator = new Authenticator<User>(sessionStorage);

authenticator.use(
  new FormStrategy(async ({ context }) => {
    if (!context?.formData) {
      throw new Error("FormData must be provided in the Context");
    }

    const formData = context.formData as FormData;

    const email = formData.get("email");
    const password = formData.get("password");

    const result = await getAccountByEmail({ email, password });

    if (!result.success) {
      throw new Error("Failed to authenticate user");
    }

    const { username, id } = result.data;

    return { username, userId: id };
  }),
  EMAIL_PASSWORD_STRATEGY
);
```

With the `authenticator` defined and everything configured we can move on to the next point.

## Creating the app pages

Let's start with the application's main route, in which we'll take advantage of the `loader` primitive together with the `authenticator` to redirect the user according to the authentication status.

```tsx
// @/app/routes/_index.tsx
import { type LoaderFunction } from "@remix-run/node";
import { route } from "routes-gen";

import { authenticator } from "~/auth/authenticator.server";

export const loader: LoaderFunction = async ({ request }) => {
  return await authenticator.isAuthenticated(request, {
    successRedirect: route("/protected"),
    failureRedirect: route("/auth/sign-up"),
  });
};
```

Next, we can move on to defining the app's layouts, starting with the authentication layout, which will only have the `Outlet` primitive to render the nested routes.

```tsx
// @/app/routes/auth.tsx
import { type V2_MetaFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";

export const meta: V2_MetaFunction = () => {
  return [{ title: "Auth Pages" }];
};

export default function AuthLayout() {
  return <Outlet />;
}
```

The next and last layout to be defined is that of protected pages, in which we are going to take advantage of the `loader` primitive and the `authenticator` to verify that the user has a valid session, if not, he is redirected to the login page. In the component's JSX we render a small component with the initials of the user's name and the `Outlet` primitive if the session is valid.

```tsx
// @/app/routes/protected.tsx
import type { LoaderArgs, V2_MetaFunction } from "@remix-run/node";
import { Outlet, useLoaderData } from "@remix-run/react";
import { route } from "routes-gen";

import { authenticator } from "~/auth/authenticator.server";

export const meta: V2_MetaFunction = () => {
  return [{ title: "Protected Pages" }];
};

export const loader = async ({ request }: LoaderArgs) => {
  return await authenticator.isAuthenticated(request, {
    failureRedirect: route("/auth/sign-in"),
  });
};

export default function ProtectedLayout() {
  const data = useLoaderData<typeof loader>();

  return (
    <div className="m-12">
      <div className="relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600">
        <span className="font-medium text-gray-600 dark:text-gray-300">
          {data.username[0].toUpperCase()}
        </span>
      </div>

      <Outlet />
    </div>
  );
}
```

Moving now to the creation of the pages themselves and so that the article is not too long, I will give a quick overview of each one.

Starting with the page for creating a new account, let's use the `loader` primitive to return the initial state of the form that will be managed using [Remix Validated Form](https://github.com/airjp73/remix-validated-form). When the form is submitted, we validate the integrity of the data and, if valid, we pass it in the arguments of the `createAccount` function. If successful, we proceed to create a user session using the `authenticator`.

```tsx
// @/app/routes/auth.sign-up.tsx
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { withZod } from "@remix-validated-form/with-zod";
import { ValidatedForm, validationError } from "remix-validated-form";
import { Link, useLoaderData } from "@remix-run/react";
import { route } from "routes-gen";

import { Input } from "~/components/Input";
import { Button } from "~/components/Button";
import type { RegisterAccountAuth } from "~/common/authSchema";
import { authSchema } from "~/common/authSchema";
import { createAccount } from "~/domain/account.server";
import {
  EMAIL_PASSWORD_STRATEGY,
  authenticator,
} from "~/auth/authenticator.server";

const validator = withZod(authSchema);

export const loader: LoaderFunction = () => {
  const defaultValues: RegisterAccountAuth = {
    username: "",
    email: "",
    password: "",
  };
  return json({ defaultValues });
};

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();

  const fieldValues = await validator.validate(formData);
  if (fieldValues.error) return validationError(fieldValues.error);

  const result = await createAccount(fieldValues.data);

  if (!result || !result.success) return null;

  return await authenticator.authenticate(EMAIL_PASSWORD_STRATEGY, request, {
    successRedirect: route("/protected"),
    context: { formData },
  });
};

export default function SignupPage() {
  const { defaultValues } = useLoaderData<typeof loader>();

  return (
    <div className="w-screen h-screen flex items-center justify-center">
      <ValidatedForm
        className="w-96 space-y-4"
        method="POST"
        validator={validator}
        defaultValues={defaultValues}
      >
        <Input
          name="username"
          label="Username"
          placeholder="Your username..."
        />
        <Input name="email" label="Email" placeholder="Your email..." />
        <Input
          name="password"
          label="Password"
          type="password"
          placeholder="Your password..."
        />
        <div className="flex items-center space-x-4">
          <Button type="submit" label="Register" />
          <Link
            to={route("/auth/sign-in")}
            className="font-medium text-blue-600 dark:text-blue-500 hover:underline"
          >
            Go to Login
          </Link>
        </div>
      </ValidatedForm>
    </div>
  );
}
```

Speaking now of the session start page, similar to the previous page, we use the `loader` primitive to return the initial state of the form and then, when submitting the data, we verify their integrity and if it is valid, we create a new session using the `authenticator`.

```tsx
// @/app/routes/auth.sign-in.tsx
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { withZod } from "@remix-validated-form/with-zod";
import { ValidatedForm, validationError } from "remix-validated-form";
import { Link, useLoaderData } from "@remix-run/react";
import { route } from "routes-gen";

import { Input } from "~/components/Input";
import { Button } from "~/components/Button";
import type { GetUserByEmailAuth } from "~/common/authSchema";
import { authSchemaWithoutUsername } from "~/common/authSchema";
import {
  EMAIL_PASSWORD_STRATEGY,
  authenticator,
} from "~/auth/authenticator.server";

const validator = withZod(authSchemaWithoutUsername);

export const loader: LoaderFunction = () => {
  const defaultValues: GetUserByEmailAuth = {
    email: "",
    password: "",
  };
  return json({ defaultValues });
};

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();

  const fieldValues = await validator.validate(formData);
  if (fieldValues.error) return validationError(fieldValues.error);

  return await authenticator.authenticate(EMAIL_PASSWORD_STRATEGY, request, {
    successRedirect: route("/protected"),
    context: { formData },
  });
};

export default function SigninPage() {
  const { defaultValues } = useLoaderData<typeof loader>();

  return (
    <div className="w-screen h-screen flex items-center justify-center">
      <ValidatedForm
        className="w-96 space-y-4"
        method="POST"
        validator={validator}
        defaultValues={defaultValues}
      >
        <Input name="email" label="Email" placeholder="Your email..." />
        <Input
          name="password"
          label="Password"
          type="password"
          placeholder="Your password..."
        />
        <div className="flex items-center space-x-4">
          <Button type="submit" label="Login" />
          <Link
            to={route("/auth/sign-up")}
            className="font-medium text-blue-600 dark:text-blue-500 hover:underline"
          >
            Go to Register
          </Link>
        </div>
      </ValidatedForm>
    </div>
  );
}
```

Last but not least we will create the main page of the protected pages layout, in which we will take advantage of the `action` primitive in which we will use the `authenticator` to remove the user session. In JSX we will have a simple form with a button to trigger the end of session.

```tsx
// @/app/routes/protected._index.tsx
import type { ActionFunction } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { route } from "routes-gen";

import { authenticator } from "~/auth/authenticator.server";
import { Button } from "~/components/Button";

export const action: ActionFunction = async ({ request }) => {
  await authenticator.logout(request, { redirectTo: route("/auth/sign-in") });
};

export default function ProtectedMain() {
  return (
    <div className="space-y-4 mt-4">
      <h1>Protected Main Page</h1>
      <small>This (nested) route is protected by the parent.</small>

      <Form method="POST">
        <Button type="submit" label="Logout" />
      </Form>
    </div>
  );
}
```

And with that I conclude the last step of this article.

## Conclusion

I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.

Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.

[Github Repo](https://github.com/FranciscoMendes10866/tiyo)