Remix.run & Django Authentication

Remix.run & Django Authentication
Photo by Joseph Corl / Unsplash

I've had a side project on the back burner for a while, a little SaaS project that I'm sure could be useful for more than just me. I had put it together with Django because that's what I'm most comfortable with, and then thrown on semblance of a frontend with the Django templating system. I got it to MVP state, and even put it out there as an investment opportunity, but ultimately it got no traction.

In the meantime, I've started a new job and been doing a lot more frontend work, which in turn has exposed my weaknesses there. I also happened across the Remix framework a couple of weeks ago and I like what they're talking about in their docs. All of those things combined have lead me to be a bit reinvigorated with the project, and so I have a few aims:

  • Adapt what's already written in Django to be headless
  • Build a new frontend in Remix to learn some new skills
  • Do a little self funded launch to see if there is any real need for the site

The first thing I wanted to get my head around was authentication – it's something that is the initial hurdle for me, because if I can conceptualize users logging in, using the site, and getting access to the data they store with the service, then I can't really progress to the other steps.

There are plenty of good how-tos out there for setting up authentication in Remix, but I couldn't find any that made it simple when it came to Django. In this post I hope to give you a starting point for how you'll need to set both up to get some authentication working that you can build from.

Django

Inside Django you'll want to install django-allauth headlessly as per their docs (you'll need to reference the non-headless docs too). You should end up with a few files including something that looks like these:

# settings.py

########################################
# AUTHENTICATION
########################################
AUTH_USER_MODEL = "users.User"
AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
)
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False

HEADLESS_ONLY = True
HEADLESS_FRONTEND_URLS = {
    "account_confirm_email": "[DOMAIN]/account/verify-email/{key}",
    "account_reset_password_from_key": "[DOMAIN]/account/password/reset/key/{key}",
    "account_signup": "[DOMAIN]/account/signup",
}
SESSION_COOKIE_DOMAIN = "itemtrak.com"
CSRF_COOKIE_DOMAIN = "itemtrak.com"
# urls.py

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("allauth.urls")),
    path("_allauth/", include("allauth.headless.urls")),
]

You'll have other things from your setup in those files, but those are some important parts. At this point Django is ready to be logged into in a headless fashion. You can check that using cURL or Postman – you'll just need to make a request to /_allauth/browser/v1/auth/login with the X-CSRFToken header set, sending csrfmiddlewaretoken, email and password in the body. You should receive back a response that shows you've been logged in.

Remix

For the Remix end, I leant heavily on the Remix Auth docs. First I used the Remix quickstart to get myself a blank Remix project, then ran through the Remix Auth steps:

return await login(email, password);

In our case, we need a login() function that interacts with our Django setup in a way that satisfies the authenticator instance that FormStrategy is using:

const login = async (
  email: FormDataEntryValue | null,
  password: FormDataEntryValue | null
) => {
  // ping the session endpoint to grab the csrf cookie
  const sessionResponse = await fetch(
    "[DOMAIN]/_allauth/browser/v1/auth/session"
  );

  // store that cookie for use in our POST request
  const csrf = getCookie(
    "csrftoken",
    sessionResponse.headers.get("Set-Cookie")!
  );

  // make the post request to the login endpoint
  const response = await fetch(
    `[DOMAIN]/_allauth/browser/v1/auth/login`,
    {
      method: "POST",
      body: JSON.stringify({
        email: email,
        password: password,
      }),
      headers: {
        "X-CSRFToken": csrf!,
        "Content-Type": "application/json",
        Cookie: `csrftoken=${csrf}`,
      },
    }
  );

  // response appropriately
  if (response.status == 200) {
    const responseData = await response.json();
    return {
      email: responseData.data.user.email,
    };
  } else {
    throw new Error("Login failed.");
  }
};

Note that this function makes a couple of calls to our backend. One of those will make a GET request to the /auth/session endpoint, and the main point of that in this instance is to get the CSRF cookie that is returned by Django. We need that in the subsequent POST request, otherwise we'll get a 403 Forbidden response from the backend no matter what we do.

The getCookie() function that's referenced there looks like this:

function getCookie(name: string, cookies: string) {
  const value = `; ${cookies}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) {
    return parts.pop()!.split(";").shift();
  }
}

When you combine that with the rest of the Remix Auth tutorial, you end up with a simple login form that will interact with Django and get you a session that is logged in.

I hope that helps someone who is trying to get something similar set up for Django and Remix. In all honesty I'm likely to move straight to login with OTP before I launch anything, but for now having something simple in place lets me get on with actually building something.