# Cookie-based Authentication in Remix

# Introduction

In this blog post, I wanna go over cookies and how to implement cookie-based authentication in React Router 7 (as a framework).

Yes, the title uses Remix since most are familiar with it. The team has now rebranded it to React Router 7. Which you now use as a library or framework.

I don't assume you're familiar with cookies. We'll start from the basics and move up from there.

I'm a big fan of learning from first principles. We'll dig into source code and specs. It's gonna be a lot of fun.

![A Contagious Smile : r/Naruto](https://preview.redd.it/xzt83rz2ea291.jpg?width=1024&format=pjpg&auto=webp&s=2b857f45d935c9378932cb4920801e13394554ac align="left")

I recommend reading MDN and documentation along the way. Read it in detail. Take your time. Make sure you understand things. Don't just read this post and "think" I covered everything.

My mindset to learning to is to be the "owner" of how I consume information. Which is why you'll see me digging into source code, specs, etc.

Remember, nothing is magic. You can learn everything. That fancy library or framework you see, whether it’s written in Rust, TypeScript, Zig, you name it, a human being wrote it.

# What are cookies?

Cookies are small pieces of data stored as text strings in the browser. The fundamental format is `name=value`. They can have additional attributes, separated by semicolons.

When a server wants to set a cookie, it sends an HTTP response header: `Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict`

The browser then stores this cookie and **automatically attaches** ([link to spec](https://datatracker.ietf.org/doc/html/rfc6265#section-4.2)) it to future requests to the same domain in the `Cookie` header: `Cookie: session=abc123`.

The automatic attachment by browsers is important. It's why cookies are good for authentication. You don't need to manually handle this in your client side code.

## Cookie attributes

### HttpOnly

JavaScript can't access the cookie, preventing [XSS attacks](https://owasp.org/www-community/attacks/xss/). XSS stands for Cross Site Scripting, it's when an attacker injects malicious code into a web page.

### Secure

Cookie only sent over HTTPS. HTTPS is the secure version of HTTP which uses [TLS encryption](https://en.wikipedia.org/wiki/Transport_Layer_Security) under the hood.

### SameSite

Controls when cookie is sent in cross-site requests. This prevents CSRF attacks. A [CSRF attack](https://owasp.org/www-community/attacks/csrf) is when an attacker tricks a user into making a POST request to a website without their knowledge.

For example, if you get an email with a button that says "click here to view your account", and you click it, the browser will automatically send the cookie with the request. The attacker can then use this to access the user's account.

It's common to set this value to `SameSite=Lax` to allow cookies to be sent in cross-site requests when GET requests are made. This way, they can remain logged in to the site.

### Domain

Which domains can receive the cookie. This is useful if you want to share cookies between subdomains.

### Path

Which paths on the domain receive the cookie.

### Expires/Max-Age

When the cookie should be deleted.

# Cookies in React Router 7 (as a framework)

In React Router 7, there are two primary ways to handle cookies: [createCookie](https://api.reactrouter.com/v7/functions/react_router.createCookie) and [createCookieSessionStorage](https://api.reactrouter.com/v7/functions/react_router.createCookieSessionStorage.html).

The full documentation for both can be found here: [React Router 7 - Sessions and Cookies](https://reactrouter.com/explanation/sessions-and-cookies).

My goal here isn't to restate everything in the docs. But I do want to go over a few things in a clearer way.

## createCookie

The source code for `createCookie` can be found here: [cookies.ts - Line 73](https://github.com/remix-run/react-router/blob/a3e4b8ed875611637357647fcf862c2bc61f4e11/packages/react-router/lib/server-runtime/cookies.ts#L73).

`createCookie` is the fundamental building block for dealing with cookies.

* Creates a single cookie with name/value
    
* Handles raw cookie operations (parse/serialize)
    
* Manages cookie attributes (expires, path, etc.)
    
* Can sign cookies for security
    
* Just manages ONE single key-value pair in the cookie
    

When is it useful?

* Simple preferences (theme, language)
    
* Single pieces of data
    

Here is a code example:

```typescript
// createCookie - theme preference
const themeCookie = createCookie("theme", {
  maxAge: 604_800, // one week
  path: "/",
});

// Usage
response.headers.set("Set-Cookie", await themeCookie.serialize("dark"));
```

## What is a "session"?

In the documentation, createCookieSessionStorage is described to provide a session-like interface. For someone who isn't familiar with the concept of a session, this can be confusing.

In traditional web development (e.g. with PHP), **a session refers to a server side storage of data that persists across requests.** For example, when you log into a PHP app, it creates a session file on the server with your data, and sends you (the client) a session ID cookie. The server uses this ID to find your data.

So it's gonna map the id to the data. On the server, the data can be stored in a database or file system. Files on disk is PHP's default.

## createCookieSessionStorage

The source code for `createCookieSessionStorage` can be found here: [cookieStorage.ts - Line 26](https://github.com/remix-run/react-router/blob/a3e4b8ed875611637357647fcf862c2bc61f4e11/packages/react-router/lib/server-runtime/sessions/cookieStorage.ts#L26).

`createCookieSessionStorage` uses `createCookie` internally. It stores all your data in one cookie. That's how it is different from the traditional session approach. Now, if you want, you can create a [custom session storage](https://reactrouter.com/explanation/sessions-and-cookies#creating-custom-session-storage).

Here is an example of how to use it from the documentation:

```typescript
import { createCookieSessionStorage } from "react-router";

type SessionData = {
  userId: string;
};

type SessionFlashData = {
  error: string;
};

const { getSession, commitSession, destroySession } =
  createCookieSessionStorage<SessionData, SessionFlashData>(
    {
      // a Cookie from `createCookie` or the CookieOptions to create one
      cookie: {
        name: "__session",

        // all of these are optional
        domain: "reactrouter.com",
        // Expires can also be set (although maxAge overrides it when used in combination).
        // Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
        //
        // expires: new Date(Date.now() + 60_000),
        httpOnly: true,
        maxAge: 60,
        path: "/",
        sameSite: "lax",
        secrets: ["s3cret1"],
        secure: true,
      },
    }
  );

export { getSession, commitSession, destroySession };
```

We touched on cookie options already e.g. `name`, `maxAge`, `path`, etc. If you want the source code for the types that React Router 7 uses, you can find it [here](https://github.com/jshttp/cookie/blob/master/src/index.ts#L161).

Let's look at two quick snippets. One that demonstrates persisting userId and the other one shows using flash messages.

## Auth example

This is just a quick example. I don't wanna dig too deep since I'm doing things a bit differently in my own project to keep it type safe.

```typescript
const { getSession, commitSession } = createCookieSessionStorage({
  cookie: {
    name: "auth",
    secrets: ["your-secret"],
    secure: true,
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 30, // 30 days
  },
});

// in your action
async function login(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));

  // here you'd get the userId from from the database
  // im just hardcoding it as demonstration
  session.set("userId", "123");

  return redirect("/dashboard", {
    headers: {
      // you always need to commit the session
      // otherwise the cookie won't be set in the headers
      // ...and the changes only remain in memory
      "Set-Cookie": await commitSession(session),
    },
  });
}

// Auth check in loaders
async function checkAuth(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");

  if (!userId) {
    return redirect("/login");
  }

  // ...
}
```

## commitSession quickly explained

`commitSession` can be a bit of a confusion. You may wonder why changes aren't set after calling `session.set()`.

In the [source code](https://github.com/remix-run/react-router/blob/a3e4b8ed875611637357647fcf862c2bc61f4e11/packages/react-router/lib/server-runtime/sessions/cookieStorage.ts#L45), you'll see that commitSession is what actually sets the cookie in the headers:

```typescript
    async commitSession(session, options) {
      let serializedCookie = await cookie.serialize(session.data, options);
      if (serializedCookie.length > 4096) {
        throw new Error(
          "Cookie length will exceed browser maximum. Length: " +
            serializedCookie.length
        );
      }
      return serializedCookie;
    }
```

## Flash messages example

```typescript
const { getSession, commitSession } = createCookieSessionStorage({
  cookie: {
    // can be any name you want e.g. toast
    name: "flash",
    secrets: ["your-secret"],
    sameSite: "lax",
  },
});

// Action that sets flash
async function saveAction(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  session.flash("success", "Changes saved successfully!");

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

// Loader that reads flash
async function loader(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  // Flash messages are removed after being read
  const message = session.get("success");

  return json(
    { message },
    {
      headers: {
        "Set-Cookie": await commitSession(session),
      },
    }
  );
}
```

# Authentication in my side project

Now it's time for the exciting stuff hihi

I wanna share how I did authentication in my side project. We're gonna cover password based authentication.

Now, I was using [Prisma as ORM](https://www.prisma.io/docs/orm/overview/introduction/what-is-prisma) and [Prisma Postgres](https://www.prisma.io/docs/orm/overview/databases/prisma-postgres) as database. You can use whatever you want though. That's just how we store data.

I'll first talk about how I deal with the cookies & redirects, then later we can look at the password stuff.

## Cookies & redirects

I'm using `createCookieSessionStorage` to handle the cookies. The relevant file for this is under [auth.server.ts](https://github.com/tigerabrodi/narumory/blob/main/app/lib/auth.server.ts).

I like to keep things type safe, so under the constants.ts file, you'll see how I deal with the cookie strings and routes:

```typescript
export const ROUTES = {
  home: '/',
  roomDetail: '/rooms/:roomCode',
  roomJoin: '/rooms/:roomCode/join',
  leaveRoom: '/rooms/:roomCode/leave',
  login: '/login',
  register: '/register',
} as const

export const COOKIE_KEYS = {
  setCookie: 'Set-Cookie',
  getCookie: 'Cookie',
} as const
```

For routes, you may wonder, how do you create a route you need to redirect to? I use `generatePath` React Router. It also infers what you should pass as params. An example: `redirect(generatePath(ROUTES.roomDetail, { roomCode: userRoom.code }))`.

The cookie keys are there so that I don't ever mistype them. If you're working on a large project in a team, these are VERY helpful.

The keys for setting and getting cookies are different. It's a bit confusing that the get one is just "Cookie", but the set one is "Set-Cookie". But that's how it is I guess.

### First bit

Let's go over the first bit of `auth.server.ts`:

```typescript
const authCookie: SessionIdStorageStrategy["cookie"] = {
  name: "__session",
  secrets: [serverEnv.SESSION_SECRET],
  sameSite: "lax",
  maxAge: 60 * 60 * 24 * 30, // 30 days
  httpOnly: true,
  secure: serverEnv.NODE_ENV === "production",
};

const cookieSchema = z.object({
  userId: z.string().optional(),
});

const sessionStorage = createCookieSessionStorage({ cookie: authCookie });

const typedAuthSessionStorage = createTypedSessionStorage({
  sessionStorage,
  schema: cookieSchema,
});

export function getCookieFromRequest(request: Request) {
  return request.headers.get(COOKIE_KEYS.getCookie);
}
```

Here I'm using [createTypedSessionStorage](https://github.com/sergiodxa/remix-utils/?tab=readme-ov-file#typed-sessions) from `remix-utils`. It's a library that provides helpers when working with React Router 7. In this case, it allows you pass a zod schema to the session storage. Keeping things type safe.

The reason userId is optional is because it could also not be there at all e.g. if user is not logged in. `SessionIdStorageStrategy` is just a type from React Router, where we access the cookie definition.

getCookieFromRequest is a helper function that gets the cookie from the request headers.

Requests and responses by the way are the normal [requests](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [responses](https://developer.mozilla.org/en-US/docs/Web/API/Response). It's not anything specific to React Router 7. I mean, it's built on top of the web platform after all.

### requireAuth and logout

```typescript
type AuthResponse =
  | {
      type: "redirect";
      response: Response;
    }
  | {
      type: "result";
      user: User;
    };

export async function requireAuth({
  request,
}: {
  request: Request;
}): Promise<AuthResponse> {
  const session = await typedAuthSessionStorage.getSession(
    getCookieFromRequest(request)
  );
  const userId = session.get("userId");

  if (!userId) {
    return {
      type: "redirect",
      response: redirect(generatePath(ROUTES.login)),
    };
  }

  const user = await prisma.user.findUnique({
    where: { id: userId },
  });

  // if user doesn't exist for whatever reason
  // logout
  if (!user) {
    return {
      type: "redirect",
      response: await logout({ request }),
    };
  }

  return {
    type: "result",
    user,
  };
}

export async function logout({ request }: { request: Request }) {
  const session = await typedAuthSessionStorage.getSession(
    getCookieFromRequest(request)
  );

  const headers = new Headers();
  headers.set(
    COOKIE_KEYS.setCookie,
    await typedAuthSessionStorage.destroySession(session)
  );

  return redirect(generatePath(ROUTES.login), {
    headers,
  });
}

// How I use requireAuth
// Can be found under e.g. app/routes/resources/api.liveblocks.ts
const requireAuthResult = await requireAuth({ request });

if (requireAuthResult.type === "redirect") return requireAuthResult.response;

const { user } = requireAuthResult;
```

Now, you can do it differently if you want. This is how I prefer to do it. And yes, it's annoying that middleware doesn't exist in React Router 7. It should be out in 2-3 months hopefully. For now, we need to handle every route ourselves.

`requireAuth` returns either a redirect or a user. To be very clear, `redirect` under the hood is just a Response. [A redirect itself is a 3XX response.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections) Source code: [utils.ts](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts#L1383).

By default, it is a 302 response. Which means a temporary redirect. This tells the browser to not cache the redirect permanently. A 301 response would be a permanent redirect, which tells the browser to cache the redirect permanently. The problem there is that if the user logs out and logs in as a different user, the browser will serve a cached redirect to the previous user's destination. Which is not what you want. 301 is suitable for e.g. static assets because they don't change.

---

Going back to the code:

```typescript
type AuthResponse =
  | {
      type: "redirect";
      response: Response;
    }
  | {
      type: "result";
      user: User;
    };
```

`requireAuth` returns one of the two types.

If the type is a redirect, we know to just return the response we get. If not, we know the user is authenticated and we can continue with the user object.

---

Let's take a look at two things in the `logout` function.

The first thing is we destroy the session. Under the hood, this is simply serializing the cookie with an empty string and making sure it expires immediately ([source code](https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/server-runtime/sessions/cookieStorage.ts#L55)).

The way I use `Headers` is something I plan on doing more often. I just find it cleaner. That's nothing from React Router 7. It's a web API. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers).

# Password based authentication

You can use popular libraries like bcrypt. I'm using the [crypto library](https://github.com/nodejs/node/blob/v20.13.1/lib/crypto.js) from node.js and doing it myself.

The key file to how I handle [password based authentication](https://github.com/tigerabrodi/narumory/blob/main/app/lib/password-service.server..ts):

```typescript
import crypto from 'crypto'

export class PasswordService {
  private static ITERATIONS = 1000
  private static KEY_LENGTH = 64
  private static ALGORITHM = 'sha256'

  static async hashPassword(password: string) {
    const salt = crypto.randomBytes(16).toString('hex')
    const hash = await this.generateHash({ password, salt })
    return { hash, salt }
  }

  static async verifyPassword({
    password,
    storedHash,
    storedSalt,
  }: {
    password: string
    storedHash: string
    storedSalt: string
  }) {
    const attemptHash = await this.generateHash({ password, salt: storedSalt })
    return attemptHash === storedHash
  }

  private static generateHash({
    password,
    salt,
  }: {
    password: string
    salt: string
  }) {
    return new Promise<string>((resolve, reject) => {
      crypto.pbkdf2(
        password,
        salt,
        this.ITERATIONS,
        this.KEY_LENGTH,
        this.ALGORITHM,
        (err, key) => {
          if (err) reject(err)
          resolve(key.toString('hex'))
        }
      )
    })
  }
}
```

I use this in:

* login: [https://github.com/tigerabrodi/narumory/blob/main/app/routes/views/auth/login.tsx](https://github.com/tigerabrodi/narumory/blob/main/app/routes/views/auth/login.tsx)
    
* create user: [https://github.com/tigerabrodi/narumory/blob/main/app/routes/views/auth/lib/db-queries.ts#L21](https://github.com/tigerabrodi/narumory/blob/main/app/routes/views/auth/lib/db-queries.ts#L21)
    

You may look at the code and go, wow, what's going on here?

Let's go over it!

## Storing passwords safely

We need a way to verify if a user's login password matches what they used when signing up. This means we need to store something. But we can't store the raw password. Because if your database gets leaked, then the attacker can get the passwords.

What we need is:

1. A function that always produces the same output for the same input (deterministic).
    
2. But is impossible to reverse - you can't go from output back to input.
    
3. So even if attackers get our database, they can't get passwords!
    

This is what a hash function does. Every time you input "password123", you get the same weird output like "a32ef7...". But you can't go backwards. That's the beauty of it.

However, simple hashing isn't enough:

1. Attackers can pre-compute hashes for common passwords.
    
2. If two users have password "123456", they'll have identical hashes (this is a problem!).
    
3. Modern computers can try billions of hashes per second.
    

This is where PBKDF2 ([Password-Based Key Derivation Function 2](https://en.wikipedia.org/wiki/PBKDF2)) comes in. It:

1. Adds a unique random value (salt) to each password before hashing
    
    * So `("password123" + "RANDOM1") ≠ ("password123" + "RANDOM2")`
        
    * This is why we use `randomBytes(16)` to create a unique salt
        
2. Hashes the result thousands of times
    
    * Each password guess now requires 1000 hashes
        
    * Makes brute force attacks thousands of times slower
        
    * This is our constant `ITERATIONS = 1000`
        
    * To be clear, after hashing the first time, **we take that result and hash again.** This is what we keep on repeating for each hash output.
        

When a user signs up:

1. Generate random salt
    
2. Run PBKDF2(password + salt) 1000 times
    
3. Store the result and salt
    

When they login:

1. Get salt from database
    
2. Run PBKDF2(login\_attempt + salt) 1000 times
    
3. If result matches stored hash, password is correct (remember, it is deterministic!)
    

## PBKDF2 parameters

PBKDF2 is recommended by NIST. You can find this publication [here](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf).

---

For ITERATIONS, a minimum of 1000 is recommended. This is what we use. On page 11 of the paper, it says:

> A minimum iteration count of 1,000 is recommended

---

On page 10, you'll see:

> All or a portion of the salt shall be generated using an approved Random Bit Generator (e.g., see \[5\]). The length of the randomly-generated portion of the salt shall be at least 128 bits.

When we do `crypto.randomBytes(16)`, we get 16 bytes. That's 128 bits.

---

ALGORITHM (SHA-256) is the specific hash function PBKDF2 uses for each iteration. SHA-256 is a cryptographically secure hash function that produces 256-bit outputs, and [is widely trusted by the security community.](https://nordvpn.com/blog/sha-256/)

---

KEY\_LENGTH (64 bytes = 512 bits) determines how long the final hash will be.

This is a bit tricky to understand. I spent like an hour researching and trying to understand what key length is actually doing here lol.

KEY\_LENGTH serves two purposes:

1. It determines the length of our final output hash
    
    * In our code it's 64 bytes (512 bits).
        
    * PBKDF2 can create this longer output by running multiple times and merging results. Otherwise, each hash output is 32 bytes (256 bits), because that's the output of SHA-256.
        
2. There's a catch though:
    
    * Because we're using SHA-256, the real security strength is limited to 256 bits.
        
    * Making it longer than 256 bits (32 bytes) does NOT add more security.
        
    * This is because HMAC-SHA256 internally works with 256-bit chunks.
        

We can ask for longer outputs, the actual security we get is the same as 256 bits. The real security comes from:

* Using a good salt
    
* Having enough iterations
    
* Users choosing strong passwords
    

# Conclusion

We went through a lot here. Way more than I expected lmao. It took me 5 hours to write this. I’m a bit stubborn when it comes to really understanding how things work.

If a single takeaway from this post, it’s gotta be the quote from [John Carmack](https://en.wikipedia.org/wiki/John_Carmack) → “Weaponize your curiosity”

![Pin page](https://i.pinimg.com/originals/43/96/86/4396860a33aa06d15dc4f353fe41e3dc.jpg align="left")
