JWT Authentication with RSA with Django

Topics covered:
- JWT Structure and how it works
- JWT User Authentication using HS256
- JWT User Authentication using RSA
- JWT User Refresh Token
Before starting…where do we want to get?
The scenario we will try to implement consists of building a django-rest-framework API that will authenticate the user using a custom username and password and return a token containing the user’s data.
In the second part of this article, we will have a look at how to refresh the token workflow.
What is JWT?
JWT (JSON Web Token) is a JSON open standard used for creating access tokens that represent a set of claims (e.g. authenticated as an admin) as a JSON object that is encoded in a JSON web signature or JSON Web Encryption structure. The information can be verified and trusted because it is digitally signed using a secret (with the HMAC algorithm) or a public/private key pair (RSA or ECDSA).
The JWT format is based on three parts:
- header: contains the algorithm used to generate the signature (e.g. HS256) and the type of token used (e.g. JWT)
- payload: contains the data representing the claims. It can contain reserved claims as in standard fields or user-defined ones (as we’re going to see later).
- signature: used to validate the token. It is formed of the encoded header and payload signed using a custom secret.
signature = HMACSHA256( base64URLEncode(header)+”.”+base64URLEncode(payload), “secret”)
The JWT token is formed by concatenating those three parts separated by “.” and each part is encoded with base64url encoding:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
If you copy and paste it to the official jwt website, you can see what your decoded token looks like:
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
How does JWT work?
The following schema is a representation of the client-server workflow. The user will POST a request to the server asking to authenticate using their username and password. If those credentials exist in the database, the server will return a JWT token containing the user’s information in the payload.
Next time the user makes a request, they would have to pass the JWT in the header with the call. The server will be configured to verify the authenticity of the incoming JWT, such that it will only accept tokens that itself created.

Setting up the DRF env
Start with a new virtual environment and install django-rest-framework-jwt
pip install djangorestframework-jwt
Configure settings.py
by adding the library to the installed apps:
INSTALLED_APPS = [
...
'rest_framework_jwt',
...
]
and add the following to the jwt configuration: your secret key used to sign the token (JWT_SECRET_KEY); the encryption algorithm (JWT_ALGORITHM) which for the moment is just going to be the default HS256; set the expiration time to 15 min (JWT_EXPIRATION_DELTA) and the refresh time to 20min (JWT_REFRESH_EXPIRATION_DELTA).
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}JWT_AUTH = {
'JWT_SECRET_KEY': 'This is a very long and secure secret key',
'JWT_GET_USER_SECRET_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=15),
'JWT_ISSUER': None,
'JWT_ALLOW_REFRESH': True,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(minutes=20),
}
Let’s set up our authentication endpoint by editing the urls.py
amd adding a login url as well as one to refresh and verify the token.
url(r'^token/obtain/$', Login.as_view(), name='authenticate-login'),
url(r'^token/refresh/', TokenRefresh.as_view()),
url(r'^token/verify/', verify_jwt_token),
and now let’s go to the main part which is our login view.
We check if a user with the given email exists and then check the password provided. If all’s good we create the payload by overriding the jwt_payload_handler
and then encode it using the jwt_encode_handler
. We will include the logged in user’s information such as the id, username, name (which contains first_name and last_name) and a set of organisations that the user is part of.
In order to allow only authenticated users to access our endpoints, we will have to add the following to the views:
authentication_classes = (JSONWebTokenAuthentication,)
permission_classes = (IsAuthenticated,)
Customise JWT to use RSA256
So far we have used the default SHA-256 symmetric algorithm which only uses one secret key that is shared between the two parties for both generating and validating the signature. Thus, a lot of care needs to be taken to not compromise the key.
Another approach would be to use an asymmetric algorithm that uses a public/private key. The server has a private key used to generate the signature and the consumer of the token gets a public key to validate the signature. The public key doesn’t need to be kept secured and thus it can be easily made available for consumers to use.
Our new workflow is shown below. A client will try and authenticate using their credentials. The server authenticates the client, generates a token and signs it using the private key. Any future requests made can be then verified using the public key.


In order to achieve this we need to make the following changes to our settings.py
where the JWT_PUBLIC_KEY_PATH and JWT_PRIVATE_KEY_PATH are the paths to your public and private keys:
JWT_AUTH = { 'JWT_PUBLIC_KEY': open(JWT_PUBLIC_KEY_PATH).read(),
'JWT_PRIVATE_KEY': open(JWT_PRIVATE_KEY_PATH).read(),
'JWT_ALGORITHM': 'RS256',
...
}
And let’s see how that would work. We log in and receive the token:

and add it as a header for all the future requests we make:

Any requests that lack an authentication header will result in an error:

Refreshing the token
A refresh token is usually long-lived and used to obtain an access token.It is issued after the authorisation process and will be used to obtain a new access token after the current token becomes invalid or expires.
django-rest-framework-jwt doesn’t support refresh tokens and allows refreshing only tokens that are still valid. One approach that we could implement is to have a sliding window such that for example the user cannot be inactive for more than five minutes in order to stay logged in.
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
Adding our new refresh token endpoint:
urlpatterns = [ ...
url(r'^api-token-refresh/', refresh_jwt_token),
]
In the next part of the article we are going to have a look at how to implement JWT Authorisation with just an API key, what are the major security concerns and how to prevent them.