Remix Authentication — Login, Session, and Protected Routes
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
- Storing passwords in plain text: Never store raw passwords. Use bcrypt or argon2 for hashing.
- Not using
httpOnlycookies: Auth cookies must behttpOnlyto prevent XSS theft. - Checking auth in components instead of loaders: Auth checks belong in loaders (server-only). Component-level checks can be bypassed.
- Missing session commit after setting data: Every
session.set()needs a correspondingcommitSession()andSet-Cookieheader. - Not handling session expiration: Show a friendly message when the session expires instead of a generic error.
Practice Questions
Where should authentication checks happen in Remix? Answer: In loaders and actions (server-side). Never trust client-side auth state for protected operations.
How do you protect a route from unauthenticated access? Answer: In the loader, check
session.has("userId")and redirect to login if missing.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.
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
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