REST framework JWT Auth

JSON Web Token Authentication support for Django REST Framework

Overview

This package provides JSON Web Token Authentication support for Django REST framework.

If you want to know more about JWT, check out the following resources:

Requirements

  • Python 2.7, 3.4+
  • Django 1.11+
  • Django REST Framework 3.7+

Security

Unlike some more typical uses of JWTs, this module only generates authentication tokens that will verify the user who is requesting one of your DRF protected API resources. The actual request parameters themselves are not included in the JWT claims which means they are not signed and may be tampered with. You should only expose your API endpoints over SSL/TLS to protect against content tampering and certain kinds of replay attacks.

Installation

Install using pip...

$ pip install drf-jwt

Add the app to your project:

INSTALLED_APPS = [
    ...
    'rest_framework_jwt',
    'rest_framework_jwt.blacklist',
    ...
]

Note the blacklist app in INSTALLED_APPS. You have to add it even if you dont't use the feature to avoid DB issues.

Usage

In your settings.py, add JSONWebTokenAuthentication to Django REST framework's DEFAULT_AUTHENTICATION_CLASSES.

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

In your urls.py add the following URL route to enable obtaining a token via a POST included the user's username and password.

from rest_framework_jwt.views import obtain_jwt_token
#...

urlpatterns = [
    '',
    # ...

    url(r'^api-token-auth/', obtain_jwt_token),
]

You can easily test if the endpoint is working by doing the following in your terminal, if you had a user created with the username admin and password password123.

$ curl -X POST -d "username=admin&password=password123" http://localhost:8000/api-token-auth/

Alternatively, you can use all the content types supported by the Django REST framework to obtain the auth token. For example:

$ curl -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"password123"}' http://localhost:8000/api-token-auth/

Now in order to access protected api urls you must include the Authorization: Bearer <your_token> header.

$ curl -H "Authorization: Bearer <your_token>" http://localhost:8000/protected-url/

In addition to adding the Authorization: Bearer in your requests, make sure that you have decorated your views properly. For this, import the JSONWebTokenAuthentication authentication class from rest_framework_jwt.authentication

from rest_framework_jwt.authentication import JSONWebTokenAuthentication

If you are using django's function based views, then you should decorate your views with the authentication_classes decorator containing JSONWebTokenAuthentication as follows:

...
@authentication_classes([JSONWebTokenAuthentication])
def protectedView(request):
    ...

In case you are using class based views, you should override the authentication_classes attribute in the view like so:

class ExampleView(APIView):
    ...
    authentication_classes = [JSONWebTokenAuthentication]
    ...

This is necessary if the JSONWebTokenAuthentication class wasn't added to the DEFAULT_AUTHENTICATION_CLASSES, or if JWT authentication is needed only on certain views. If you have added it to DEFAULT_AUTHENTICATION_CLASSES, then the views will be authenticated using JWT authentication automatically.

Refresh Token

If JWT_ALLOW_REFRESH is True, non-expired tokens can be "refreshed" to obtain a brand new token with renewed expiration time. Add a URL pattern like this:

    from rest_framework_jwt.views import refresh_jwt_token
    #  ...

    urlpatterns = [
        #  ...
        url(r'^api-token-refresh/', refresh_jwt_token),
    ]

Pass in an existing token to the refresh endpoint as follows: {"token": EXISTING_TOKEN}. Note that only non-expired tokens will work. The JSON response looks the same as the normal obtain token endpoint {"token": NEW_TOKEN}.

$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-refresh/

Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain of token stores the time that the original token (obtained with username/password credentials), as orig_iat. You can only keep refreshing tokens up to JWT_REFRESH_EXPIRATION_DELTA.

A typical use case might be a web app where you'd like to keep the user "logged in" the site without having to re-enter their password, or get kicked out by surprise before their token expired. Imagine they had a 1-hour token and are just at the last minute while they're still doing something. With mobile you could perhaps store the username/password to get a new token, but this is not a great idea in a browser. Each time the user loads the page, you can check if there is an existing non-expired token and if it's close to being expired, refresh it to extend their session. In other words, if a user is actively using your site, they can keep their "session" alive.

Verify Token

In some microservice architectures, authentication is handled by a single service. Other services delegate the responsibility of confirming that a user is logged in to this authentication service. This usually means that a service will pass a JWT received from the user to the authentication service, and wait for a confirmation that the JWT is valid before returning protected resources to the user.

This setup is supported in this package using a verification endpoint. Add the following URL pattern:

    from rest_framework_jwt.views import verify_jwt_token

    #...

    urlpatterns = [
        #  ...
        url(r'^api-token-verify/', verify_jwt_token),
    ]

Passing a token to the verification endpoint will return a 200 response and the token if it is valid. Otherwise, it will return a 400 Bad Request as well as an error identifying why the token was invalid.

$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-verify/

Impersonation Token

Impersonation allows the service to perform actions on the client’s behalf. A typical use case would be troubleshooting. We can act like the user who submitted an issue without requiring its login credentials.

By default, only superusers (user.is_superuser == True) can impersonate other accounts. If you need to customize the permission handling process, override the ImpersonateJSONWebTokenView's permission_classes attribute.

Blacklisting Tokens

Blacklisting allows users to blacklist their own token from the HTTP header or cookies. General use case is as a logout service.

There are two options for blacklisting tokens: 1. Using Blacklist as a logout service - make a POST request to the BlacklistView and the token will be retrieved from the request. 2. Blacklisting specific tokens - make a POST request to the BlacklistView with the body

{'token' : '<token_to_be_blacklisted>'}

For example, your urls.py may look like:

from rest_framework_jwt.blacklist.views import BlacklistView
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path("auth/", obtain_jwt_token),
    path("auth/logout/", BlacklistView.as_view({"post": "create"}))
]

delete_stale_tokens management command

When called, deletes all blacklisted tokens that have expired.

Warning

Unless JWT_TOKEN_ID is set to require, blacklisting tokens will store the entire token value. This creates a potential problem if someone is able to read and delete records from the blacklist, either directly in the database or via the administrative interface. Note that the default value is include, not require. See the section on JWT_TOKEN_ID for how to migrate to requiring token id claims in all tokens.

Additional Settings

There are some additional settings that you can override similar to how you'd do it with Django REST framework itself. Here are all the available defaults.

JWT_AUTH = {
    'JWT_SECRET_KEY': settings.SECRET_KEY,
    'JWT_GET_USER_SECRET_KEY': None,
    'JWT_PRIVATE_KEY': None,
    'JWT_PUBLIC_KEY': None,
    'JWT_ALGORITHM': 'HS256',
    'JWT_INSIST_ON_KID': False,
    'JWT_TOKEN_ID': 'include',
    'JWT_AUDIENCE': None,
    'JWT_ISSUER': None,
    'JWT_ENCODE_HANDLER':
        'rest_framework_jwt.utils.jwt_encode_payload',
    'JWT_DECODE_HANDLER':
        'rest_framework_jwt.utils.jwt_decode_token',
    'JWT_PAYLOAD_HANDLER':
        'rest_framework_jwt.utils.jwt_create_payload',
    'JWT_PAYLOAD_GET_USERNAME_HANDLER':
        'rest_framework_jwt.utils.jwt_get_username_from_payload_handler',
    'JWT_PAYLOAD_INCLUDE_USER_ID': True,
    'JWT_VERIFY': True,
    'JWT_VERIFY_EXPIRATION': True,
    'JWT_LEEWAY': 0,
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
    'JWT_ALLOW_REFRESH': True,
    'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
    'JWT_AUTH_HEADER_PREFIX': 'Bearer',
    'JWT_RESPONSE_PAYLOAD_HANDLER':
        'rest_framework_jwt.utils.jwt_create_response_payload',
    'JWT_AUTH_COOKIE': None,
    'JWT_AUTH_COOKIE_DOMAIN': None,
    'JWT_AUTH_COOKIE_PATH': '/',
    'JWT_AUTH_COOKIE_SECURE': True,
    'JWT_AUTH_COOKIE_SAMESITE': 'Lax',
    'JWT_IMPERSONATION_COOKIE': None,
    'JWT_DELETE_STALE_BLACKLISTED_TOKENS': False,
}

This package uses the JSON Web Token Python implementation, PyJWT and allows to modify some of its available options.

JWT_SECRET_KEY

This is the secret key used to sign the JWT. Make sure this is safe and not shared or public.

Can be a dict, a list or a scalar.

  • When a dict, the dict keys are taken as the JWT key ids and the values as keys, e.g.:

python { "kid1": key1, "kid2": key2, ... }

The first element is used for signing.

If a JWT to be verified contains a key id (kid header), only the key with that id is tried (if any).

NOTE: For python < 3.7, use a collections.OrderedDict object, e.g.:

```python
from collections import OrderedDict

JWT_AUTH["JWT_SECRET_KEY"] = OrderedDict(kid1=key1, kid2=key2, ...)
```
  • When a list, all elements are accepted for verification and the first element is used for signing.

  • When a scalar, this secret is used for signing and verification.

(The first) JWT_SECRET_KEY is only used for signing if (the first) JWT_ALGORITHM is HS*, otherwise JWT_PRIVATE_KEY is used.

JWT_SECRET_KEY(s) is/are only used for verification of JWTs with alg matching HS*

Default is your project's settings.SECRET_KEY.

JWT_GET_USER_SECRET_KEY

This is more robust version of JWT_SECRET_KEY. It is defined per User, so in case token is compromised it can be easily changed by owner. Changing this value will make all tokens for given user unusable. Value should be a function, accepting user as the only parameter and returning its secret key as string.

Default is None.

JWT_PRIVATE_KEY

Can be a scalar or a dict.

When a dict, the dict key is taken as the JWT key id and the values as the key, e.g.:

{ "kid": key }

The scalar or the dict value must be in any private key format supported by PyJWT, for example of the types

  • cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey
  • cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey

And will be used to sign the signature component of the JWT if JWT_ALGORITHM is set to any of the supported algorithms other than the hash types HS*.

Default is None.

JWT_PUBLIC_KEY

Can be a scalar, a list or a dict.

  • When a dict, the dict keys are taken as the JWT key ids and the values as keys, e.g.:

python { "kid1": key1, "kid2": key2, ... }

If a JWT that contains a key id (kid header) is to be verified, only the associated key is tried. Otherwise, or

  • when a list, all of the elements will be accepted for verification of JWTs with alg being (any of) JWT_ALGORITHM not matching HS*.

The scalar or elements/values of the list/dict must be in any public key format supported by PyJWT, for example of the types

  • cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey
  • cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey

Default is None.

JWT_ALGORITHM

Possible values are any of the supported algorithms for cryptographic signing in PyJWT.

Can be a scalar or a list.

  • For a scalar, this algorithm is used for signing and verification.

  • For a list, the first element is used for signing and all elements are accepted for verification.

Default is "HS256".

JWT_INSIST_ON_KID

When key IDs are used (JWT_SECRET_KEY and/or JWT_PUBLIC_KEY given as a dict assigning key IDs to keys), insist that JWTs to be validated have a kid header with a defined key.

Default is False.

JWT_TOKEN_ID

Configure whether tokens have a jti token id claim (and refreshed tokens have a orig_jti claim).

May be set to:

  • off: do not include token id claims in tokens
  • include: add token id claims to tokens, but continue accepting old tokens without them
  • require: add token id claims to tokens, and reject tokens that lack them

The default has been to include these claims since version 1.17.

For new installations, please override the default and set this to require, as every token will have an id from the outset.

For existing installations, when migrating from an older version (pre-1.17) or when changing the setting from off, we recommend setting this to require once all of the valid tokens have the id claims. This will typically be after JWT_EXPIRATION_DELTA has elapsed since upgrading or allowing id claims to be included.

Note that when set to off or include, the blacklist functionality - if used - will store the entire token value, which would allow someone with access to the administrative interface, or directly to the database, to steal an otherwise valid token and remove it from the blacklist. Using require for this setting means that only token identifiers are recorded for the blacklist and not entire tokens.

Default is include.

JWT_AUDIENCE

This is a string that will be checked against the aud field of the token, if present.

Default is None (fail if aud present on JWT).

JWT_ISSUER

This is a string that will be checked against the iss field of the token.

Default is None (do not check iss on JWT).

JWT_ENCODE_HANDLER

Encodes JWT payload data and returns JWT token.

JWT_DECODE_HANDLER

Decodes JWT token and returns JWT payload data.

JWT_PAYLOAD_HANDLER

Specify a custom function to generate the token payload

JWT_PAYLOAD_GET_USERNAME_HANDLER

If you store username differently than the default payload handler does, implement this function to fetch username from the payload.

JWT_PAYLOAD_INCLUDE_USER_ID

If you do not wish to include the user's primary key (typically id) in the token payload, then set this to False.

Default is True.

JWT_VERIFY

If the secret is wrong, it will raise a jwt.DecodeError. You can still get the payload by setting the JWT_VERIFY to False.

Default is True.

JWT_VERIFY_EXPIRATION

You can turn off expiration time verification by setting JWT_VERIFY_EXPIRATION to False. Without expiration verification, JWTs will last forever meaning a leaked token could be used by an attacker indefinitely.

Default is True.

JWT_LEEWAY

This allows you to validate an expiration time which is in the past but not very far. For example, if you have a JWT payload with an expiration time set to 30 seconds after creation but you know that sometimes you will process it after 30 seconds, you can set a leeway of 10 seconds in order to have some margin.

Default is 0 seconds.

JWT_EXPIRATION_DELTA

This is an instance of Python's datetime.timedelta. This will be added to datetime.utcnow() to set the expiration time.

Default is datetime.timedelta(seconds=300) (5 minutes).

JWT_ALLOW_REFRESH

Enable token refresh functionality. Token issued from rest_framework_jwt.views.obtain_jwt_token will have an orig_iat field.

Default is True.

JWT_REFRESH_EXPIRATION_DELTA

Limit on token refresh, is a datetime.timedelta instance. This is how much time after the original token that future tokens can be refreshed from.

Default is datetime.timedelta(days=7) (7 days).

JWT_AUTH_HEADER_PREFIX

You can modify the Authorization header value prefix that is required to be sent together with the token.

Default value is Bearer.

JWT_RESPONSE_PAYLOAD_HANDLER

Creates a response payload instance that will get passed to authentication response serializer. You might want to implement your own handler if you use custom response serializer (typical use-case would be including serialized user object in response).

By default returns a namedtuple with attributes pk (issued-at time) and token.

Example:

def jwt_create_response_payload(token, user=None, request=None, issued_at=None):
    """
    Return data ready to be passed to serializer.

    Override this function if you need to include any additional data for
    serializer.

    Note that we are using `pk` field here - this is for forward compatibility
    with drf add-ons that might require `pk` field in order (eg. jsonapi).
    """

    response_payload = namedtuple('ResponsePayload', 'pk token user')
    response_payload.pk = issued_at
    response_payload.token = token
    response_payload.user = user

    return response_payload

You can set this to a string if you want to use http cookies in addition to the Authorization header as a valid transport for the token. The string you set here will be used as the cookie name that will be set in the response headers when requesting a token. The token validation procedure will also look into this cookie, if set. The 'Authorization' header takes precedence if both the header and the cookie are present in the request.

Default is None and no cookie is set when creating tokens nor accepted when validating them.

Default: None

The domain to use for the JWT cookie analogous to SESSION_COOKIE_DOMAIN for django sessions.

Has no effect unless JWT_AUTH_COOKIE is set.

Default: /

The path to set on the JWT cookie analogous to SESSION_COOKIE_PATH for django sessions.

Has no effect unless JWT_AUTH_COOKIE is set.

Default: True

Whether to use a secure cookie for the JWT cookie analogous to SESSION_COOKIE_SECURE for django sessions.

Users wishing to use JWT cookies over http (as in no TLS/SSL) need to set JWT_AUTH_COOKIE_SECURE to False.

Has no effect unless JWT_AUTH_COOKIE is set.

Default: Lax

The value of the SameSite flag on the the JWT cookie analogous to SESSION_COOKIE_SAMESITE for django sessions.

Has no effect unless JWT_AUTH_COOKIE is set.

Has no effect with Django versions before 2.1.

Analogous to the JWT_AUTH_COOKIE setting, but contains the impersonation token, i.e. the token of the user who is being impersonated.

This cookie takes precedence over the JWT_AUTH_COOKIE. If you have both cookies and you want to end the impersonation, you have to remove the cookie.

Impersonation cookies use the JWT_AUTH_COOKIE_* settings.

JWT_DELETE_STALE_BLACKLISTED_TOKENS

Enables deleting of stale blacklisted tokens on post_save when set to True. All blacklisted tokens that have expired will be deleted.

Default is False.

Extending/Overriding JSONWebTokenAuthentication

Right now JSONWebTokenAuthentication assumes that the JWT will come in the header, or a cookie if configured (see JWT_AUTH_COOKIE). The JWT spec does not require this (see: Making a service Call). For example, the JWT may come in the querystring. The ability to send the JWT in the querystring is needed in cases where the user cannot set the header (for example the src element in HTML).

To achieve this functionality, the user might write a custom Authentication class:

class JSONWebTokenAuthenticationQS(JSONWebTokenAuthentication):

    def get_jwt_value(self, request):
        return request.QUERY_PARAMS.get('jwt')

Creating a new token manually

Sometimes you may want to manually generate a token, for example to return a token to the user immediately after account creation. You can do this as follows:

from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

Creating a new token on the command line

There is a management command which you can use to obtain a JWT token, for example:

python manage.py obtain_token username=John
# or
python manage.py obtain_token pk=1

it could be easier during development, to run this management command instead of using login API.