Server-Side Authentication

with React Router v7, Firebase, and Remix-Utils

Server-side authentication is critical for securely managing user sessions in modern web applications. In this in-depth guide, you'll learn how to implement robust server-side authentication using React Router v7, Firebase Authentication, and Remix-Utils for added security measures such as CSRF protection.

Setting Up Your Project

Step 1: Install Dependencies

Before starting, ensure your project has the necessary dependencies:

npm install firebase remix-utils react-router drizzle-orm

Step 2: Create a Firebase Project

  • Go to Firebase Console and create a new project.
  • Enable Authentication methods (e.g., Email/Password, Google OAuth).
  • Copy your project's config details.

Step 3: Initialize Firebase in Your App

Create a Firebase initialization file:

// lib/firebase/firebase.server.ts
import admin from "firebase-admin";

if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert({ /* your Firebase credentials */ }),
  });
}

export const serverAuth = admin.auth();

Creating the Login Page

Create your login layout using React Router's structured routes:

// routes/sign-in/page.tsx
import { Outlet } from "react-router";

export default function LoginPage() {
  return (
    <div className="login-container">
      <Outlet />
    </div>
  );
}

Setting Up Authentication Layout

Create a wrapper layout that ensures all private routes are protected:

// routes/navigation/nav-layout.tsx
import { Outlet, redirect } from "react-router";
import { getUserDataSession } from "~/lib/get-user-data-session";

export async function loader({ request }) {
  const { userData } = await getUserDataSession(request);
  if (!userData) {
    return redirect("/sign-in");
  }
  return { userData };
}

export default function NavLayout({ loaderData }) {
  return (
    <div>
      <nav>User: {loaderData.userData.email}</nav>
      <Outlet />
    </div>
  );
}

Adding Authentication Loader

Implement an authentication loader to verify user sessions:

// lib/get-user-data-session.ts
import { getSession } from "~/lib/session/session.server";
import { serverAuth } from "~/lib/firebase/firebase.server";

export async function getUserDataSession(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const sessionCookie = session.get("sessionCookie");
  if (sessionCookie) {
    try {
      const userData = await serverAuth.verifySessionCookie(sessionCookie, true);
      return { session, userData };
    } catch {
      session.unset("sessionCookie");
    }
  }
  return { session, userData: null };
}

CSRF Protection

Implement CSRF protection using Remix-Utils:

// utils/csrf.server.ts
import { CSRF } from "remix-utils/csrf/server";
import { createCookie } from "react-router";

export const csrfCookie = createCookie("csrf", { httpOnly: true, secure: true });

export const csrf = new CSRF({
  cookie: csrfCookie,
  formDataKey: "csrfToken",
  secret: "your-secret",
});

Apply CSRF protection in your root layout:

// routes/root.tsx
import { AuthenticityTokenProvider } from "remix-utils/csrf/react";

export async function loader({ request }) {
  const [token, cookieHeader] = await csrf.commitToken();
  return new Response(JSON.stringify({ token }), { headers: { "Set-Cookie": cookieHeader } });
}

export default function App({ loaderData }) {
  return (
    <AuthenticityTokenProvider token={loaderData.token}>
      <Outlet />
    </AuthenticityTokenProvider>
  );
}

Handling Login Actions

Securely process login actions and establish user sessions:

// routes/sign-in/action.ts
import { redirect } from "react-router";
import { csrf } from "~/lib/session/csrf.server";
import { serverAuth } from "~/lib/firebase/firebase.server";

export async function action({ request }) {
  await csrf.validate(request);
  const formData = await request.formData();
  const idToken = formData.get("idToken")?.toString();

  if (idToken) {
    const sessionCookie = await serverAuth.createSessionCookie(idToken, { expiresIn: 604800000 }); // 7 days
    const session = await getSession();
    session.set("sessionCookie", sessionCookie);

    return redirect("/", { headers: { "Set-Cookie": await commitSession(session) } });
  }

  return new Response("Invalid login.", { status: 400 });
}

Refreshing Sessions

Implement a session-refresh route to keep sessions valid:

// routes/api/action.refresh-session.ts
import { redirect } from "react-router";
import { serverAuth } from "~/lib/firebase/firebase.server";

export async function action({ request }) {
  const session = await getSession(request.headers.get("Cookie"));
  const idToken = (await request.formData()).get("idToken")?.toString();

  if (!idToken) return redirect("/");

  const sessionCookie = await serverAuth.createSessionCookie(idToken, { expiresIn: 604800000 });
  session.set("sessionCookie", sessionCookie);

  return new Response("Session refreshed.", { headers: { "Set-Cookie": await commitSession(session) } });
}

Best Practices and Considerations

  • Always validate CSRF tokens for state-changing requests.
  • Secure cookies using httpOnly, secure, and appropriate sameSite settings.
  • Regularly refresh session cookies to maintain security.

By following this guide, you've set up a robust and secure server-side authentication system, protecting your React Router v7 application while providing a smooth user experience.