How toBuild a simple login and logout with Remix

If you want to add a simple login and logout behavior to your Remix application, you can build one with a few lines of code by using Remix built-in session storage helpers.

First create your SessionStorage instance.

app/session.server.ts
import { createCookieSessionStorage } from "@remix-run/node"; type SessionData = { userId: string; role: "user" | "admin" }; export const sessionStorage = createCookieSessionStorage<SessionData>({ cookie: { // Name of the session cookie, use whatever you want here name: "session", // This configures the cookie so it's not accessible with `document.cookie` httpOnly: true, // This configures the cookie so it's only sent over HTTPS when running in // production. When running locally, it's sent over HTTP too secure: process.env.NODE_ENV === "production", // This configures the path where the cookie will be available, / means // everywhere path: "/", // This secrets are used to sign the cookie, preventing any tampering secrets: [process.env.SESSION_SECRET ?? "s3cr3t"], }, }); export const { getSession, commitSession, destroySession } = sessionStorage;

The Register Route

Now let's create a /register route.

app/routes/register.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form } from "@remix-run/react"; import { getSession, commitSession } from "~/session.server"; import { createUser } from "~/models/user.server"; // mock file export async function loader({ request }: LoaderFunctionArgs) { let session = await getSession(request.headers.get("cookie")); // redirect to / if the user is logged-in if (session.has("userId") return redirect("/") return json(null); } export async function action({ request }: ActionFunctionArgs) { let formData = await request.formData(); let user = await createUser({ email: formData.get("email"), password: formData.get("password") // createUser must encrypt the password }) let session = await getSession(request.headers.get("cookie")); session.set("userId", user.id); session.set("role", user.role); return redirect("/", { headers: { "set-cookie": await commitSession(session) } }); } export default function Component() { return ( <Form method="post"> <label id="email">Email</label> <input id="email" type="email" name="email" required /> <label id="password">Password</label> <input id="password" type="password" name="password" required /> <button>Register</button> </Form> ); }

This file reference a mock ~/models/user.server, here you need to add the logic that given an email and password strings, you must.

  1. Check if there's no user with that email
  2. Encrypt the password
  3. Create the user in the DB using the email and encrypted password
  4. Return the user data

If any step fails you should throw an error and ideally show the error to the user in the UI.

The Login Route

Now let's create the /login route.

app/routes/login.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form } from "@remix-run/react"; import { getSession, commitSession } from "~/session.server"; import { findUser } from "~/models/user.server"; // mock file export async function loader({ request }: LoaderFunctionArgs) { let session = await getSession(request.headers.get("cookie")); // redirect to / if the user is logged-in if (session.has("userId") return redirect("/") return json(null); } export async function action({ request }: ActionFunctionArgs) { let formData = await request.formData(); let user = await findUser({ email: formData.get("email"), password: formData.get("password") }) let session = await getSession(request.headers.get("cookie")); session.set("userId", user.id); session.set("role", user.role); return redirect("/", { headers: { "set-cookie": await commitSession(session) } }); } export default function Component() { return ( <Form method="post"> <label id="email">Email</label> <input id="email" type="email" name="email" required /> <label id="password">Password</label> <input id="password" type="password" name="password" required /> <button>Log In</button> </Form> ); }

As you can see the code is almost the same, the difference here is that instead of createUser we call findUser from our mock `~/models/user.server" file.

Inside findUser you need to add the logic that given an email and password strings, you must.

  1. Check if there's an user with that email
  2. Encrypt the password
  3. Check if the encrypted password match the user password in the DB
  4. Return the user data

If something fails you need to show the errors to the UI, otherwise the user is logged-in now.

The Logout Route

Finally, let's create a /logout route.

app/routes/logout.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirectDocument } from "@remix-run/node"; import { Form } from "@remix-run/react"; import { getSession, destroySession } from "~/session.server"; export async function loader({ request }: LoaderFunctionArgs) { let session = await getSession(request.headers.get("cookie")); // redirect to / if the user is not logged-in if (!session.has("userId") return redirect("/") return json(null); } export async function action({ request }: ActionFunctionArgs) { let session = await getSession(request.headers.get("cookie")); return redirectDocument("/", { headers: { "set-cookie": await destroySession(session) } }); } export default function Component() { return ( <Form method="post"> <button>Log Out</button> </Form> ); }

Here we need to show a simple form that only confirms the logout, the reason to ask for confirmation rather than doing the logout on a GET is for security reasons.

After the user submit the form, we get the session and destroy it. Then we also do a redirectDocument, the difference with a normal redirect is that this version cause a new document request to the server instead of performing a client-side navigation.

We do this to ensure that any possible client-side state is wiped out from the memory and the user is in a new clean state.