djwto: Django Authentication with JWT
Introducing an alternative implementation for the auth layer using JWTs.
I usually write about data science projects. So it begs the question: “How come this post can be about adding a JWT based auth layer on top of Django?”
Well, what I can say is, on each project that we work on, the need to offer customers some sort of a simple and yet effective online portal where they can interact with the ML models became quite clear.
And that’s basically the scenario that sets my journey on developing djwto (“jot two”) which stands as an alternative JWT auth implementation for Django. One of the steps for building the front-end is a proper auth layer and we chose using JWTs for doing so.
There’re currently already plenty of great packages out there, djwto basically adds some new features whilst aiming for simplicity and being lightweight. Among some new possibilities:
- djwto offers the option of splitting the access token in two parts (that’s the main inspiration for its name) where one piece is the decoded payload.
- Works with either bearer tokens or cookies.
- CSRF protected by default.
- Offers decorators for protecting views (full authentication and authorization layers).
- Fully customizable.
In this post, let’s see a brief introduction of djwto and how it can help developers to add the auth layer on their projects as well. We’ll build a Django project and interact with some of the aforementioned features along the way.
1. The Environment
We first need a Django project to begin with so let’s create one (for what follows it’s assumed a unix based system is used).Change directory to one that is empty and run the commands:
Feel free to use another version of Python (it can be either 3.7 or 3.9); this is what we have so far:
.
├── djwto_project
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
Now let’s modify urls.py
to include djwto’s urls:
It’s also possible to modify settings.py
to configure how djwto should run. Here’s a simple example:
First we add "djwto"
to INSTALLED_APPS
(here "sslserver"
has been added as well just for testing with https, it’s not necessary in practice).
Each setting value should be intuitive for the most part. A few of them is worth further discussing:
DJWTO_SIGNING_KEY
should contain the key for encoding the tokens and by default is expected to be available as an environmental variable. If using asymmetrical cryptography, thenDJWTO_VERIFYING_KEY
should also be set.DJWTO_MODE
sets how the JWTs will be handled to the client. djwto works with two tokens: access and refresh. They are essentially the same thing but the former is short-lived and should be “refreshed” by using the latter, which in turn is long-lived. The mode of operation can be one of the following:
-JSON
: The JWTs are returned to the client as regular JSONs.
-ONE-COOKIE
: The JWTs are stored in cookies.
-TWO-COOKIES
: Similar to before but this time the access token is split into two parts. One is not encoded and therefore its payload is accessible by the client.
Further explanation for each setting is available in the official docs.
A user will also be required in the database; feel free to create one using your favorite method. Let’s run the migrations first:
export DJWTO_SIGNING_KEY=’test key’ && python manage.py makemigrations
python manage.py migrate
And then let’s use the shell
:
python manage.py shell
Run the following:
Now the database contains the user alice which we’ll be using for the auth verification step.
Finally, let’s create a new app for our project where we’ll interact with djwto. Just run in your terminal:
python manage.py startapp testapp
Let’s put djwto to the test now!
2. Running djwto
To begin with, let’s use the requests
library and send a login POST request with the user alice to see how it goes. First we need to run the server:
export DJWTO_SIGNING_KEY=’test key’ && python manage.py runsslserver 0.0.0.0:8002
Here we exported the value of the signing key and used sslserver
for the https encryption management, all that on port 8002
(trying to pick a somewhat unusual port).
2.1 Login
Here’s the Python code to send a login
request
Now if the mode at with djwto is running is JSON then the result should be both access and refresh tokens being retrieved in the response:
Let’s go ahead and change the settings at which djwto is running and set its mode to the TWO-COOKIES option:
#./djwto_project/settings.pyDJWTO_MODE = ‘TWO-COOKIES’
The same code now will return something different. The sess
variable will contain cookies with csrftoken
and the JWTs:
sess
<RequestsCookieJar[ Cookie(name=’csrftoken’, value=’mzv…’), Cookie(name=’jwt_access_payload’, value=’eyJ…'), Cookie(name=’jwt_access_token’, value=’eyJ’, rest={‘HttpOnly’: None}), Cookie(name=’jwt_refresh’, value=’eyJ’, path=’/api/token/refresh’, rest={‘HttpOnly’: None})]>
jwt_access_payload contains a base64
encoded value of the token payload. In fact, in order to retrieve the original value, just decode it:
This value can be used directly by the front-end client (still be careful to not store sensitive information there).
Let’s see now how we’d perform token validation to confirm the JWTs are still good.
2.2 Validate
Given that the tokens were created, at times it’s desired to check if they are still valid at some point. Suppose we are in JSON mode:
Notice the token is simply added to the header AUTHORIZATION
following the Bearer pattern. This should return:
For ONE-COOKIE or TWO-COOKIES modes, here’s how to do it:
Notice the csrf token is sent in the header X-CSRFToken
as well as a REFERER
indicating from where the request came from (this is necessary due to Django’s security system).
Validating the refresh token is a bit different:
The rest of all endpoints are fully documented in the official docs.
3. Protecting Views
djwto offers direct protection of views by requiring the JWTs being available in the input request. For testing that, let’s create a view in our testapp like so:
Notice the @method_decorator(auth.jwt_login_required)
and auth.jwt_perm_required
. When decorating views with those, the view will only be processed if the request contains an authenticating JWT.
Create a file urls.py
in testapp so we can route to those views, like so:
Update the project urls file to include the newly added testapp:
# ./djwto_project/urls.py
from django.urls import path, includeurlpatterns = [
path('', include('djwto.urls')),
path('', include('testapp.urls'))
]
Now we can send the GET request to our server:
Which returns: "worked!"
. If we remove the JWTs from the request, here’s what happens:
Each view containing the jwt_login_required
is fully protected now; the same can be used for user permissions.
4. Customization
djwto was built aiming for being customizable. Let’s see an example. Using our testapp, let’s update the files apps.py
to change how the users are processed when creating the tokens:
Notice we import djwto.tokens
and change the function that processes the users. Now, when the user logs in, a new field email
will show up in the tokens; you can change its behavior for better suiting your needs. Here’s an example result of a new token:
{
"aud": "aud",
"exp": 1624259339,
"iat": 1624229339,
"iss": "iss",
"jti": "900f4f1a-3e0f-4843-9997-9fd8d032684e",
"refresh_iat": 1624229339,
"sub": "sub",
"type": "access",
"user": {
"email": "alice@djwto.com",
"id": 1,
"perms": [],
"username": "alice"
}
}
5. Conclusion
This was djwto in a nutshell. Many other features are fully documented in the official repo:
There you’ll find more about signals, endpoints, customizations, managing the two-cookies settings and much more. Feel free to check it out!