Skip to content

Remix Authentication — Login, Session, and Protected Routes

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Remix Authentication. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Remix authentication: implement login/logout, session-based auth, protected routes with middleware, and role-based access control for secure applications.

In this lesson, you'll build a complete authentication system with login, session management, protected routes, and role-based access.

What You'll Learn

How to implement login with session storage, protect routes with loader checks, implement role-based access, and handle logout securely.

Why It Matters

Authentication is essential for most web applications. Remix's session API and server-only loaders make auth naturally secure since auth logic never reaches the client.

Real-World Use

DodaZIP uses session-based auth with role checks (admin, editor, viewer) for its multi-tenant admin dashboard.

flowchart LR
    A[Login Form] --> B[Action: Validate]
    B -->|Success| C[Create Session]
    B -->|Fail| D[Show Error]
    C --> E[Redirect to Dashboard]
    E --> F[Loader: Check Session]
    F -->|Valid| G[Render Page]
    F -->|Invalid| H[Redirect to Login]
    style B fill:#121212,color:#fff

Session Setup

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

export const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: "_auth",
      httpOnly: true,
      maxAge: 60 * 60 * 24 * 7,
      path: "/",
      sameSite: "lax",
      secrets: [process.env.SESSION_SECRET],
      secure: process.env.NODE_ENV === "production",
    },
  });

Login Action

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

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

  const user = await verifyLogin(email, password);
  
  if (!user) {
    session.flash("error", "Invalid email or password");
    return json({ error: "Invalid credentials" }, {
      headers: { "Set-Cookie": await commitSession(session) },
    });
  }

  session.set("userId", user.id);
  session.set("role", user.role);

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

Protecting Routes

Check session in loaders:

import { redirect } from "@remix-run/node";
import { getSession } from "~/session.server";

export const loader = async ({ request }) => {
  const session = await getSession(request.headers.get("Cookie"));

  if (!session.has("userId")) {
    session.flash("error", "You must be logged in");
    return redirect("/login", {
      headers: { "Set-Cookie": await commitSession(session) },
    });
  }

  const userId = session.get("userId");
  const user = await getUserById(userId);

  return json({ user });
};

Role-Based Access

export const loader = async ({ request }) => {
  const session = await getSession(request.headers.get("Cookie"));
  const role = session.get("role");

  if (role !== "admin") {
    throw new Response("Access denied", { status: 403 });
  }

  return json(await getAdminData());
};

Logout

export const action = async ({ request }) => {
  const session = await getSession(request.headers.get("Cookie"));

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

Reusable Auth Hook

// app/hooks/useUser.ts
import { useRouteLoaderData } from "@remix-run/react";

export function useUser() {
  const data = useRouteLoaderData("root");
  return data?.user;
}

Common Mistakes

  1. Storing passwords in plain text: Never store raw passwords. Use bcrypt or argon2 for hashing.
  2. Not using httpOnly cookies: Auth cookies must be httpOnly to prevent XSS theft.
  3. Checking auth in components instead of loaders: Auth checks belong in loaders (server-only). Component-level checks can be bypassed.
  4. Missing session commit after setting data: Every session.set() needs a corresponding commitSession() and Set-Cookie header.
  5. Not handling session expiration: Show a friendly message when the session expires instead of a generic error.

Practice Questions

  1. Where should authentication checks happen in Remix? Answer: In loaders and actions (server-side). Never trust client-side auth state for protected operations.

  2. How do you protect a route from unauthenticated access? Answer: In the loader, check session.has("userId") and redirect to login if missing.

  3. How do you implement role-based access? Answer: Store the user's role in the session. Check it in the loader and throw a 403 Response if unauthorized.

  4. What happens to the session cookie on logout? Answer: destroySession() clears the cookie. The browser removes it from storage.

Challenge

Build a complete auth system with login, signup, protected dashboard, admin-only admin panel, and role-based middleware that prevents viewers from accessing edit routes.

Mini Project

Create a multi-tenant app where users have roles (admin, editor, viewer). Admin can manage all content, editor can create/edit but not delete, viewer can only read. Protect every route with role checks.

FAQ

Should I use JWT or sessions in Remix?

: Sessions are simpler and more secure for server-rendered apps. JWTs are useful for API authentication consumed by mobile apps.

How do I handle password reset?

: Create a flow with a reset token, email sending, and a form that updates the password. Store the reset token temporarily.

Can I use OAuth providers like Google or GitHub?

: Yes. Implement OAuth by redirecting to the provider, handling the callback, and creating a session.

How do I protect API routes (resource routes)?

: Same as page routes. Check the session in the loader or action and throw 401/403 if unauthorized.

What's Next

Learn about Remix Mutations for complex data mutation patterns beyond basic form handling.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro