Authenticating Remix cookie sessions with Django cookie sessions
Tags: remix, webdev, session, cookie
Published: 2022-01-17 17:02:35

## Prerequisites

This post assumes you have knowledge of

- Remix sessions
- an external backend service that already manages user cookie sessions/authentication (like Django sessions)

## Anecdote

I recently converted an app at work to Remix, and I wanted to share one of the issues I had ran into and solved. The app's backend was in Django, and its user authentication/sessions were also handled by Django. There was no problem with user authentication back when the app was an SPA since the browser was directly sending the fetch calls to the backend. It became a problem though when using Remix loader/action to fetch data from the backend; the fetch calls did not know which user was logged in when sending requests to my external backend service. After some experimentation, I've come up with a solution. I had to duplicate the cookie session handling in Remix:

1. In the Remix loader/action fetch calls, take the response and parse the headers's `Set-Cookie`.
2. Create a Remix cookie session storage that handle encoding/decoding the session values.
3. Create new headers and return the headers with the response in the loader/action.

More details down below.

## Create a session storage in Remix to store the session ID

I first had to create a cookie session storage in Remix. The name of the cookie had to match the name of the cookie header from my Django backend. In this case, it's `sessionid`.

```ts
// app/session.server.ts

export const sessionIdSessionStorage = createCookieSessionStorage({
  cookie: {
    name: 'sessionid',
    path: '/',
    secrets: [SESSION_SECRET],
    secure: process.env.NODE_ENV === 'production',
    // More cookie options could be added here, but we will be using the cookie options from the fetch responses later.
  },
})
```

## Fetching the login API in Remix action

My app had a login route with an action that takes the username and password to log the user in. The important part here is in the `action` of the route.

```ts
// app/routes/login.tsx

import type { ActionFunction, CookieSerializeOptions } from 'remix'
import setCookie from 'set-cookie-parser'
import { sessionIdSessionStorage } from '~/session.server'

const LOGIN_QUERY = /* GraphQL */ `
  mutation Login($username: string, $password: string) {
    ...
  }
`

export const action: ActionFunction = ({ request }) => {
  const formData = await request.formData()
  const username = formData.get('username')
  const password = formData.get('password')
  const response = await fetch('https://my-backend.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: LOGIN_QUERY,
      variables: {
        username,
        password,
      }
    })
  })

  if (response.status !== 200) {
    return ...
  }

  // Ensure that the response has a `Set-Cookie` header. It's required for every subsequent request that requires authentication.
  const setCookieHeader = response.headers.get('Set-Cookie')

  if (!setCookieHeader) {
    // No set cookie header meant something went wrong. Return an error.
    return ...
  }

  // Parse the cookies and find the `sessionid` cookie.
  const parsedResponseCookies = setCookie.parse(setCookie.splitCookiesString(setCookieHeader))
  const sessionIdCookie = parsedResponseCookies.find((cookie) => cookie.name === 'sessionid')

  if (!sessionIdCookie) {
    // No `sessionid` cookie in the fetch response means something went wrong.
    return ...
  }

  // Create the headers that will need to be returned to the browser. These headers are needed for every subsequent request that require authentication.
  const headers = new Headers()

  // Store the response's `sessionid` cookie into the headers.
  const { name, value, ...sessionIdCookieSerializeOptions } = sessionIdCookie
  const sessionIdSession = await sessionIdSessionStorage.getSession(request.headers.get('Cookie'))
  sessionIdSession.set(name, value)
  headers.append(
    'Set-Cookie',
    await sessionIdSessionStorage.commitSession(
      sessionIdSession,
      // Use the response's `sessionid` cookie serialization options.
      sessionIdCookieSerializeOptions as CookieSerializeOptions,
    ),
  )

  // Make sure to return a response with the headers you just created.
  return redirect("/profile", { headers })
}

const Login = () => {
  const actionData = useActionData()
  return ...
}

export default Login
```

Now after a user logs in, in the browser's Application -> Cookies tab, you should see the `sessionid` key value there. With every fetch request, the browser sends this `sessionid` in its header cookies.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfpgo14um6d4z7tc6prh.png)

## Subsequent fetch requests that requires user to be authenticated

I had to redirect the user to another route after the user logs in. That route requires some user-specific data. How can we tell Remix that the user is logged in? We need to tell Remix to read the requests' `Set-Cookie` and forward the decoded values in the fetch requests:

```ts
// app/routes/profile.tsx

import type { LoaderFunction } from 'remix'
import { json, redirect, useLoaderData } from 'remix'
import { sessionIdSessionStorage } from '~/session.server'

const PROFILE_QUERY = /* GraphQL */ `
  query Profile {
    ...
  }
`

export const loader: LoaderFunction = ({ request }) => {
  // The browser should automatically send the `sessionid` header cookie when this route is loaded.
  const sessionIdSession = await sessionIdSessionStorage.getSession(request.headers.get('Cookie'))

  // Ensure that the `sessionid` is present.
  if (!sessionIdSession.has('sessionid')) {
    // If it is not present, then the user has not been logged in.
    throw redirect('/login')
  }

  const response = await fetch('https://my-backend.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      // IMPORTANT: send the `sessionid` cookie with the fetch request! This will tell your backend that the fetch is user-authenticated.
      Cookie: Object.entries(sessionIdSession.data)
        .map(([key, value]) => `${key}=${value}`)
        .join('; '),
    },
    body: JSON.stringify({
      query: PROFILE_QUERY,
    })
  })

  // Add some checks to make sure the fetch went through without errors.
  if (!response.ok || response.status !== 200) {
    // Return some errors or throw a redirect.
    return ...
  }

  const result = await response.json()

  // OPTIONAL: There's a chance that your backend returned new `sessionid` cookie headers in the profile query fetch as well. You could perform the same steps from the login action to store the new `sessionid` cookie in the browser.
  // const headers = new Headers()
  // const setCookieHeader = response.headers.get('Set-Cookie')
  // ...
  // return json(result, { headers })

  return json(result)
}

const Profile = () => {
  const loaderData = useLoaderData()
  return ...
}

export default Profile
```

I've created abstractions for the cookie parsing so that I don't have to repeat those steps in every loader/action, and if you're doing this too, I'd recommend you do the same.