Skip to content

Using Basic Auth with Remix

No need to pay for a simple password-protected website, use the platform

  1. Add the required environment variables to your .env file.
  2. Create the utility function to authenticate the user.
app/utils/auth.server.ts
import { json } from "@remix-run/node";
 
const VALID_USERNAME = process.env.VALID_USERNAME;
const VALID_PASSWORD = process.env.VALID_PASSWORD;
 
export async function authenticateUser(request: Request) {
  const authHeader = request.headers.get("Authorization");
 
  if (!authHeader) {
    throw json("Unauthorized", {
      status: 401,
      statusText: "Unauthorized",
    });
  }
 
  const [scheme, credentials] = authHeader.split(" ");
 
  if (scheme !== "Basic") {
    throw json("Invalid authentication scheme", {
      status: 400,
      statusText: "Bad Request",
    });
  }
 
  const [username, password] = Buffer.from(credentials, "base64")
    .toString()
    .split(":");
 
  if (username !== VALID_USERNAME || password !== VALID_PASSWORD) {
    throw json("Invalid credentials", {
      status: 401,
      statusText: "Unauthorized",
    });
  }
 
  return true;
}

Call the authenticateUser function in the loader of the protected page. Handle the happy path, as the errors will be guided to the ErrorBoundary.

app/routes/my_page.tsx
import {
  HeadersFunction,
  json,
  type LoaderFunction,
  type MetaFunction,
} from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { authenticateUser } from "~/utils/auth.server";
 
export const loader: LoaderFunction = async ({ request }) => {
  await authenticateUser(request);
 
  return json({ data: "My data" });
};
 
export const meta: MetaFunction = () => {
  return [
    { title: "Protected page example" },
    { name: "description", content: "Protected page example" },
  ];
};
 
export default function MyPage() {
  const { data } = useLoaderData<typeof loader>();
 
  return <div>{data}</div>;
}
 
// Move this to `root` if you want to protect all routes
export const headers: HeadersFunction = () => ({
  "WWW-Authenticate": "Basic",
});

Nothing fancy in the root, maybe add the Header here for global protection. Also ensure to have a proper error boundary in place.

app/root.tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
export { ErrorBoundary } from "./components/ErrorBoundary";
 
import "./tailwind.css";
 
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
 
export function App() {
  return <Outlet />;
}
app/components/ErrorBoundary.tsx
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
 
const Container = ({ children }: { children: React.ReactNode }) => {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-950 space-y-2">
      {children}
    </div>
  );
};
 
const Title = ({ children }: { children: React.ReactNode }) => {
  return <h1 className="text-4xl text-gray-100">{children}</h1>;
};
 
const Subtitle = ({ children }: { children: React.ReactNode }) => {
  return <h2 className="text-2xl text-gray-400">{children}</h2>;
};
 
export function ErrorBoundary() {
  const error = useRouteError();
 
  if (isRouteErrorResponse(error)) {
    return (
      <Container>
        <Title>
          {error.status} {error.statusText}
        </Title>
        <Subtitle>{error.data}</Subtitle>
      </Container>
    );
  } else if (error instanceof Error) {
    return (
      <Container>
        <Title>Error</Title>
        <Subtitle>{error.message}</Subtitle>
      </Container>
    );
  } else {
    return (
      <Container>
        <Title>Unknown Error</Title>
        <Subtitle>An unknown error occurred</Subtitle>
      </Container>
    );
  }
}