This is part 2 of a two-part series. If you haven't done so, I recommend you read Part 1 first.
In the first part, we created a new service, Auth, that would authenticate a user and then store information about them in cookies using JSON Web Tokens. This post is about authenticating users in our other services, by replacing the default Django Authentication Middleware.
This article is about how to pick up Django users from the JWT data in the previous post, so you can use Django to render templates, manage users, etc. If you don’t want or need the Django Authentication system and, for example, only want to authenticate API requests, check out the SimpleJWT documentation — you might find all you need already there.
Let’s go to one of our services and add a new app, called jwtauthmiddleware
. (Later, I recommend you put that app into its own repository and install it via pip, but this is a good way of stepping through the code.)
$ cd some-service/
$ ./manage.py startapp jwtauthmiddleware
Let’s write a middleware! Django middlewares, at their easiest, consist of a class that contains one method, process_request
, which takes a request
object and modifies it somehow. In our case we are going to instantiate a user from the JWT data and set it as request.user
, very much like the normal AuthenticationMiddleware
might do.
Inside the jwtauthmiddleware
package, create a new class for our middleware, for example in __init__.py
:
class JWTAuthenticationMiddleware(AuthenticationMiddleware):
def get_user(self, request):
# Retrieve the token from cookie
access = request.COOKIES.get("org.breakthesystem.jwt.access")
refresh = request.COOKIES.get("org.breakthesystem.jwt.refresh")
# Check for invalid or expired token
try:
token = AccessToken(access)
except TokenError:
return AnonymousUser()
# Retrieve Token Payload Data
user_uuid = token.payload.get("user_uuid")
user_email = token.payload.get("user_email")
user_is_staff = token.payload.get("user_is_staff")
user_is_superuser = token.payload.get("user_is_superuser")
user_first_name = token.payload.get("user_first_name")
user_last_name = token.payload.get("user_last_name")
# Make sure the the payload data is actually present
for variable in [access, refresh, user_uuid, user_email, user_is_staff, user_is_superuser]:
if variable is None:
return AnonymousUser()
# Create a new user. There's no need to set a
# password because it is not used to log in directly
user, created = User.objects.get_or_create(username=user_uuid)
# Update the user's meta information, the access
# token payload is the canonical source of truth
user.email = user_email
user.is_staff = user_is_staff
user.is_superuser = user_is_superuser
user.first_name = user_first_name
user.last_name = user_last_name
user.save()
return user or AnonymousUser()
def process_request(self, request):
request.user = SimpleLazyObject(lambda: self.get_user(request))
In the previous article, we saved the user’s username
into a dictionary field called user_uuid
, because we have replaced usernames with randomly generated IDs. Here we take the user_uuid
field from the dictionary and save it as username
again.
This helps us here: We don’t have to care about database IDs and still are able to have unique usernames. If you want to display a user’s identifying information, you should use their first name, last name, or email address.
If in the previous post you decided to use actual usernames, this code should work just as well. You might want to consider renaming the user_uuid
dictionary key though, as it is misleading in this case.
In your project’s settings.py
, change the MIDDLEWARE
setting. Remove the entry 'django.contrib.auth.middleware.AuthenticationMiddleware'
, and instead add 'jwtauthmiddleware.JWTAuthenticationMiddleware'
.
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
# Remove this
# 'django.contrib.auth.middleware.AuthenticationMiddleware',
# Replace by this
"jwtauthmiddleware.JWTAuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
Also, we need to set a LOGIN_URL
to the URL where our Auth service lives. This will redirect users who are not logged in to the Auth service URL.
We also define another setting, OWN_URL
, for reasons that will become clear in a minute.
LOGIN_URL = "https://auth.breakthesystem.org/login"
OWN_URL = "https://someservice.breakhtesystem.org"
The middleware is all we really need at this point. Now, if your user is logged in to your Auth service, things will work exactly as expected.
If your user is not logged in, they will be redirected to the Auth service URL. However, right now, they won’t get redirected back after login. Why is that?
By default, when Django redirects you to the LOGIN_URL
, it appends the current path as a parameter called next
. The URL looks like this:
https://auth.breakthesystem.org/login?next=/home/user/daniel
If authentication lives inside the same service, this next
path is enough. However, we are changing URLs here, so the Auth service has not enough information to redirect our users back to where they need to be. What we’d like instead is that the next
parameter contains the whole URL, including host, of the current service.
So, let’s do that! We are going to use monkey patching to replace the next implementation to include our service’s host.
from django.contrib.auth import views as auth_views
REDIRECT_FIELD_NAME = "next"
def custom_redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
"""
Redirect the user to the login page, passing the given 'next' page.
"""
resolved_url = settings.LOGIN_URL
login_url_parts = list(urlparse(resolved_url))
if redirect_field_name:
querystring = QueryDict(login_url_parts[4], mutable=True)
querystring[redirect_field_name] = settings.DOMAIN + next
login_url_parts[4] = querystring.urlencode(safe="/")
redirect_url = urlunparse(login_url_parts)
return HttpResponseRedirect(redirect_url)
auth_views.redirect_to_login = custom_redirect_to_login
By now you should have a working JWT Auth service that saves all necessary user information in cookies. You also created a Django Authentication Middleware replacement which will extract a User object from the stored cookie and saves it into request.user
.
If you have questions or comments about this article, please contact me on twitter.