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.
jwtauthmiddleware package, create a new class for our middleware, for example in
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
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
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:
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, mutable=True) querystring[redirect_field_name] = settings.DOMAIN + next login_url_parts = 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
If you have questions or comments about this article, please contact me on twitter.