Skip to content
This repository was archived by the owner on May 26, 2020. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain

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.
Expand All @@ -116,6 +117,30 @@ Passing a token to the verification endpoint will return a 200 response and the
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-verify/
```

## Blacklist Token
If `rest_framework_jwt.blacklist` is added to `settings.INSTALLED_APPS`, tokens can be made invalid (prior to expiration) by blacklisting them. More information on the concept of JTI (JWT ID) and blacklisting tokens can be read [here](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#jtiDef) and [here](https://auth0.com/blog/2015/03/10/blacklist-json-web-token-api-keys/).

This package comes with a default implementation that stores the blacklisted tokens in the configured django database and includes an admin integration.

To use this feature, add a URL like so:

```python
url(r'^api-token-blacklist/', 'rest_framework_jwt.blacklist.views.blacklist_jwt_token'),
```

Now to blacklist a token, send a POST request with a non-expired token to the blacklist endpoint:

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

If the blacklisting was successful, the response will contain the default implementation response data which is the token and a success message. Any future requests using that token will be denied.

The typical use case for this feature is forcefully logging a user out due to inactivity. Many applications, especially ones with sensitive information, may implement an activity-based countdown timer and wish to instantly inactivate the user's auth token. The default implementation stores a record in the database with the unique JTI (JWT ID). However there are configurable handlers for getting and setting the blacklisted token which leaves it up to the user to decide how or where they are stored. The only requirements are that both `JWT_BLACKLIST_GET_HANDLER` and `JWT_BLACKLIST_SET_HANDLER` return a valid blacklisted token or None.

**Note**
Applications that are built with a Serivce-Oriented-Architecture (SOA) may not be able to use the blacklist token feature due to having to query the auth service on every request to check if the JWT is blacklisted.

## 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.

Expand All @@ -136,6 +161,15 @@ JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',

'JWT_BLACKLIST_GET_HANDLER':
'rest_framework_jwt.blacklist.utils.jwt_blacklist_get_handler',

'JWT_BLACKLIST_SET_HANDLER':
'rest_framework_jwt.blacklist.utils.jwt_blacklist_set_handler',

'JWT_BLACKLIST_RESPONSE_HANDLER':
'rest_framework_jwt.blacklist.utils.jwt_blacklist_response_handler',

'JWT_SECRET_KEY': settings.SECRET_KEY,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
Expand Down Expand Up @@ -208,6 +242,9 @@ Default is `datetime.timedelta(days=7)` (7 days).
### JWT_PAYLOAD_HANDLER
Specify a custom function to generate the token payload

**Note**
If you have `rest_framework_jwt.blacklist` added to `INSTALLED_APPS` *AND* you are using the default blacklist implementation, any custom token payload must include both a `jti` attribute which is a unique UUID hex, and a `exp` attribute which is a timestamp from a POSIX time (e.g. seconds since epoch).

### JWT_PAYLOAD_GET_USER_ID_HANDLER
If you store `user_id` differently than the default payload handler does, implement this function to fetch `user_id` from the payload.

Expand All @@ -227,6 +264,50 @@ def jwt_response_payload_handler(token, user=None, request=None):

Default is `{'token': token}`

### JWT_BLACKLIST_GET_HANDLER
Responsible for fetching a blacklisted JWT token. This function should return either a valid blacklisted token or None.

The default included implementation is as follows:
```
def jwt_blacklist_get_handler(payload):
jti = payload.get('jti')
try:
token = models.JWTBlackListToken.objects.get(jti=jti)
except models.JWTBlackListToken.DoesNotExist:
return None
else:
return token
```

### JWT_BLACKLIST_SET_HANDLER
Responsible for setting a blacklisted JWT token. This function should return either a valid blacklisted token or None.

The default included implementation is as follows:
```
def jwt_blacklist_set_handler(payload):
try:
data = {
'jti': payload.get('jti'),
'created': now(),
'expires': datetime.fromtimestamp(payload.get('exp'))
}
return models.JWTBlackListToken.objects.create(**data)
except (TypeError, IntegrityError, Exception):
return None
```

### JWT_BLACKLIST_RESPONSE_HANDLER
Controls what the response data for a request to the JWT blacklist endpoint returns.

The default implementation is as follows:
```
def jwt_blacklist_response_handler(token, user=None, request=None):
return {
'token': JWTBlackListTokenSerializer(token).data,
'message': 'Token successfully blacklisted.'
}
```

### JWT_AUTH_HEADER_PREFIX
You can modify the Authorization header value prefix that is required to be sent together with the token. The default value is `JWT`. This decision was introduced in PR [#4](https://github.com/GetBlimp/django-rest-framework-jwt/pull/4) to allow using both this package and OAuth2 in DRF.

Expand Down
12 changes: 12 additions & 0 deletions rest_framework_jwt/authentication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import jwt

from django.conf import settings
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _

from rest_framework import exceptions
from rest_framework.authentication import (BaseAuthentication,
get_authorization_header)
Expand All @@ -11,6 +14,7 @@

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER
jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER


class BaseJSONWebTokenAuthentication(BaseAuthentication):
Expand Down Expand Up @@ -38,6 +42,14 @@ def authenticate(self, request):
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()

# Check if the token has been blacklisted.
if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS:
blacklisted = jwt_blacklist_get_handler(payload)

if blacklisted:
msg = _('Token has been blacklisted.')
raise exceptions.AuthenticationFailed(msg)

user = self.authenticate_credentials(payload)

return (user, jwt_value)
Expand Down
Empty file.
24 changes: 24 additions & 0 deletions rest_framework_jwt/blacklist/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db.models import get_model
from django.conf import settings
from django.contrib import admin

from . import models


class JWTBlacklistTokenAdmin(admin.ModelAdmin):
list_display = ('jti', 'expires', 'created', 'is_active')
fields = ('jti', 'expires', 'created', 'is_active')
readonly_fields = ('jti', 'expires', 'created', 'is_active')

def is_active(self, obj):
return obj.is_active()
is_active.boolean = True
is_active.short_description = 'Active'

if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS:
try:
get_model('blacklist', 'jwtblacklisttoken')
except Exception:
pass
else:
admin.site.register(models.JWTBlacklistToken, JWTBlacklistTokenAdmin)
20 changes: 20 additions & 0 deletions rest_framework_jwt/blacklist/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.db import models
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

from rest_framework_jwt.compat import get_uuid_field

UUIDField = get_uuid_field()


class JWTBlacklistToken(models.Model):
jti = UUIDField()
expires = models.DateTimeField()
created = models.DateTimeField(auto_now_add=True)

class Meta:
verbose_name = _('JWT Blacklist Token')
verbose_name_plural = _('JWT Blacklist Tokens')

def is_active(self):
return self.expires > now()
46 changes: 46 additions & 0 deletions rest_framework_jwt/blacklist/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django.conf import settings
from django.utils.translation import ugettext as _

from rest_framework import serializers

from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.serializers import VerificationBaseSerializer

from . import models

jwt_blacklist_set_handler = api_settings.JWT_BLACKLIST_SET_HANDLER


class BlacklistJSONWebTokenSerializer(VerificationBaseSerializer):
"""
Blacklist an access token.
"""
def validate(self, attrs):

token = attrs['token']

if 'rest_framework_jwt.blacklist' not in settings.INSTALLED_APPS:
msg = _('The blacklist app is not installed.')
raise serializers.ValidationError(msg)

payload = self._check_payload(token=token)

# Handle blacklisting a token.
token = jwt_blacklist_set_handler(payload)

if not token:
msg = _('Could not blacklist token.')
raise serializers.ValidationError(msg)

user = self._check_user(payload=payload)

return {
'token': token,
'user': user
}


class JWTBlacklistTokenSerializer(serializers.ModelSerializer):
class Meta:
model = models.JWTBlacklistToken
exclude = ('id',)
52 changes: 52 additions & 0 deletions rest_framework_jwt/blacklist/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from datetime import datetime

from django.db import IntegrityError
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

from . import models


def jwt_blacklist_get_handler(payload):
"""
Default implementation to check if a blacklisted jwt token exists.

Should return a black listed token or None.
"""
jti = payload.get('jti')

try:
token = models.JWTBlacklistToken.objects.get(jti=jti)
except models.JWTBlacklistToken.DoesNotExist:
return None
else:
return token


def jwt_blacklist_set_handler(payload):
"""
Default implementation that blacklists a jwt token.

Should return a black listed token or None.
"""
try:
data = {
'jti': payload.get('jti'),
'created': now(),
'expires': datetime.fromtimestamp(payload.get('exp'))
}
return models.JWTBlacklistToken.objects.create(**data)
except (TypeError, IntegrityError, Exception):
return None


def jwt_blacklist_response_handler(token, user=None, request=None):
"""
Default blacklist token response data. Override to provide a
custom response.
"""
from . import serializers
return {
'token': serializers.JWTBlacklistTokenSerializer(token).data,
'message': _('Token successfully blacklisted.')
}
19 changes: 19 additions & 0 deletions rest_framework_jwt/blacklist/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from rest_framework_jwt.settings import api_settings
from rest_framework_jwt.views import JSONWebTokenAPIView

from . import serializers

jwt_blacklist_response_handler = api_settings.JWT_BLACKLIST_RESPONSE_HANDLER


class BlacklistJSONWebToken(JSONWebTokenAPIView):
"""
API View that blacklists a token
"""
serializer_class = serializers.BlacklistJSONWebTokenSerializer
response_payload_handler = staticmethod(
jwt_blacklist_response_handler
)


blacklist_jwt_token = BlacklistJSONWebToken.as_view()
20 changes: 19 additions & 1 deletion rest_framework_jwt/compat.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import rest_framework

from django.db import models

from distutils.version import StrictVersion
from functools import partial

from rest_framework import serializers


if StrictVersion(rest_framework.VERSION) < StrictVersion('3.0.0'):
from rest_framework.serializers import Serializer
else:
class Serializer(rest_framework.serializers.Serializer):
class Serializer(serializers.Serializer):
@property
def object(self):
return self.validated_data


def get_uuid_field():
"""
Returns a partial object that when called instantiates a UUIDField
either from Django 1.8's native implementation, or as a CharField.
"""
if hasattr(models, 'UUIDField'):
return partial(models.UUIDField, editable=False, unique=True)
else:
return partial(models.CharField, max_length=64,
editable=False, unique=True)
16 changes: 14 additions & 2 deletions rest_framework_jwt/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
from calendar import timegm
from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.auth import authenticate
from django.utils.translation import ugettext as _
from rest_framework import serializers
from .compat import Serializer

from rest_framework import exceptions
from rest_framework import serializers
from rest_framework_jwt import utils
from rest_framework_jwt.settings import api_settings

from .compat import Serializer

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_user_id_from_payload = api_settings.JWT_PAYLOAD_GET_USER_ID_HANDLER
jwt_blacklist_get_handler = api_settings.JWT_BLACKLIST_GET_HANDLER


class JSONWebTokenSerializer(Serializer):
Expand Down Expand Up @@ -103,6 +106,14 @@ def _check_payload(self, token):
msg = _('Error decoding signature.')
raise serializers.ValidationError(msg)

# Check if the token has been blacklisted.
if 'rest_framework_jwt.blacklist' in settings.INSTALLED_APPS:
blacklisted = jwt_blacklist_get_handler(payload)

if blacklisted:
msg = _('Token has been blacklisted.')
raise exceptions.AuthenticationFailed(msg)

return payload

def _check_user(self, payload):
Expand Down Expand Up @@ -150,6 +161,7 @@ def validate(self, attrs):

payload = self._check_payload(token=token)
user = self._check_user(payload=payload)

# Get and check 'orig_iat'
orig_iat = payload.get('orig_iat')

Expand Down
Loading