Remix.run & Django Authentication
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.