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
Show all changes
99 commits
Select commit Hold shift + click to select a range
b175f39
Added support for jti claim
avimeir Mar 22, 2015
003e805
reducing the probability of repeated keys by using the user_id:jti pa…
avimeir Mar 23, 2015
8c23e9e
Merge branch 'jti'
avimeir Mar 23, 2015
dd86025
Merge branch 'master' of https://github.com/avimeir/django-rest-frame…
Mar 27, 2015
0c0f946
Adds JWTBlackListToken model and default implementation for blacklist…
Mar 27, 2015
caaa6eb
Fixed typo
Mar 27, 2015
e08ad93
Disable blacklist tests temporarily
Mar 27, 2015
09d5b8a
Added fallback for import error
Mar 27, 2015
4bc7e96
Fixed typo
Mar 27, 2015
1eaf24f
flake8
Mar 27, 2015
dda8303
Cleanup
Mar 27, 2015
c592c25
Use uuid.hex
Mar 27, 2015
8a52647
Moved uuid import to compat
Mar 27, 2015
9fc7750
Cleanup
Mar 28, 2015
2b80040
Added blacklisted token check to authentiate method to verify incomin…
Mar 28, 2015
d63cb70
Fixed import
Mar 28, 2015
303dd59
Changed ValidationError to Authorization exception
Mar 28, 2015
fda33b3
Cleanup
Mar 28, 2015
0452dbe
Added expires_at field to JWTBlackListToken model
Mar 28, 2015
e857b80
Code tweaks
Mar 28, 2015
35f7017
cleanup
Mar 28, 2015
75e4f4d
Fix expires_at
Mar 28, 2015
849f879
Changed back to return None
Mar 28, 2015
84b4baa
Added admin for default implementation
Mar 28, 2015
8e29a5c
Added pytz to req
Mar 28, 2015
48b26e6
Make admin dependent on settings
Mar 28, 2015
5a8496d
Updated tests and requirements
Mar 29, 2015
b5e5f49
Refactored view to allow custom payload handler, added default black …
Mar 29, 2015
20799c3
Updated tox
Mar 29, 2015
568efb9
Add check for successful blacklist token
Mar 29, 2015
16f16df
Fix oauth import in tox
Mar 29, 2015
e664e1c
Removed pytz and django-uuidfields
Mar 30, 2015
8b42c64
Removed one last pytz import
Mar 30, 2015
b553d93
Added blacklist auth test
Mar 31, 2015
aba07a0
Added another auth test
Mar 31, 2015
4272c04
Added other blacklist unit tests
Apr 1, 2015
7df1149
Updated documentation and did another pass through cleanup
Apr 1, 2015
d0d1626
Proofread docs
Apr 1, 2015
6ecdf7f
Update index.md
Apr 1, 2015
e7c3d22
Update index.md
Apr 1, 2015
10ab7e0
Changed model method to is_active
Apr 1, 2015
00dbd12
Changed model method to is_active
Apr 1, 2015
ef176c0
Separated blacklist token feature into subapp
Apr 3, 2015
30b9531
Separated blacklist view and utils into the subapp, updated docs
Apr 4, 2015
4bff8e3
Removed JWT_ENABLE_BLACKLIST - tests broken
Apr 4, 2015
41e9955
Fixed typo in blacklist serializer, removed blacklist disabled test a…
Apr 5, 2015
8bd31fe
Merged and fixed tests, updated readme
Apr 6, 2015
8ac3f8b
Fixed merge requirements issue
Apr 6, 2015
ad15329
Update readme
Apr 6, 2015
5fa6077
Updated readme and admin
Apr 21, 2015
f47aec1
Merge pull request #108 from ticosax/speedup-tests
jpadilla May 1, 2015
7c037c7
Merge pull request #109 from ticosax/verify-should-not-refresh
jpadilla May 2, 2015
83451de
Added support for jti claim
avimeir Mar 22, 2015
14debb4
reducing the probability of repeated keys by using the user_id:jti pa…
avimeir Mar 23, 2015
15c9b13
Adds JWTBlackListToken model and default implementation for blacklist…
Mar 27, 2015
319c6a2
Fixed typo
Mar 27, 2015
05b0839
Disable blacklist tests temporarily
Mar 27, 2015
b097594
Added fallback for import error
Mar 27, 2015
2e118c0
Fixed typo
Mar 27, 2015
84c72ea
flake8
Mar 27, 2015
c44bd87
Cleanup
Mar 27, 2015
906896d
Use uuid.hex
Mar 27, 2015
391928f
Moved uuid import to compat
Mar 27, 2015
f56e955
Cleanup
Mar 28, 2015
4b56adc
Added blacklisted token check to authentiate method to verify incomin…
Mar 28, 2015
6425a2e
Fixed import
Mar 28, 2015
3d4c00b
Changed ValidationError to Authorization exception
Mar 28, 2015
c7c04c5
Cleanup
Mar 28, 2015
6294afa
Added expires_at field to JWTBlackListToken model
Mar 28, 2015
617c436
Code tweaks
Mar 28, 2015
255a91a
cleanup
Mar 28, 2015
285cfb2
Fix expires_at
Mar 28, 2015
df83277
Changed back to return None
Mar 28, 2015
e6c3843
Added admin for default implementation
Mar 28, 2015
8efe5ca
Added pytz to req
Mar 28, 2015
4312e87
Make admin dependent on settings
Mar 28, 2015
27667e1
Updated tests and requirements
Mar 29, 2015
7eb1019
Refactored view to allow custom payload handler, added default black …
Mar 29, 2015
335bc62
Add check for successful blacklist token
Mar 29, 2015
c399a3a
Fix oauth import in tox
Mar 29, 2015
14085e5
Removed pytz and django-uuidfields
Mar 30, 2015
ec73b3e
Added blacklist auth test
Mar 31, 2015
8ce2fe2
Added another auth test
Mar 31, 2015
29bdfa6
Added other blacklist unit tests
Apr 1, 2015
cc3c32a
Updated documentation and did another pass through cleanup
Apr 1, 2015
62efabc
Proofread docs
Apr 1, 2015
869e902
Update index.md
Apr 1, 2015
5ca120d
Update index.md
Apr 1, 2015
a12f8e7
Changed model method to is_active
Apr 1, 2015
73d3d1a
Changed model method to is_active
Apr 1, 2015
f0f6b57
Separated blacklist token feature into subapp
Apr 3, 2015
baa5cac
Separated blacklist view and utils into the subapp, updated docs
Apr 4, 2015
ce72d61
Removed JWT_ENABLE_BLACKLIST - tests broken
Apr 4, 2015
fc99489
Fixed typo in blacklist serializer, removed blacklist disabled test a…
Apr 5, 2015
e9760dc
Fixed merge requirements issue
Apr 6, 2015
d65459e
Update readme
Apr 6, 2015
ae33ba4
Updated readme and admin
Apr 21, 2015
975811a
Clean whitespace
May 4, 2015
c03f6f1
Merge with jti
May 4, 2015
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
83 changes: 83 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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would add something about significant overhead to query the auth service on every request to check if the JWT is blacklisted. (Which defeats the purpose of using JWT's anyway)

## 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',

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same about all these utils... should they belong in the blacklist app as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm they could, although it might be easier to keep them grouped with the other utils(?) would like @jpadilla input on this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it does make sense to separate the views and utils into the subapp as well so its completely decoupled

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I guess it makes sense for these to be separate as well.

'JWT_SECRET_KEY': settings.SECRET_KEY,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
Expand All @@ -149,6 +183,8 @@ JWT_AUTH = {
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

'JWT_AUTH_HEADER_PREFIX': 'JWT',

'JWT_ENABLE_BLACKLIST': False,
}
```
This packages uses the JSON Web Token Python implementation, [PyJWT](https://github.com/jpadilla/pyjwt) and allows to modify some of it's available options.
Expand Down Expand Up @@ -208,6 +244,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 +266,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
20 changes: 20 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,22 @@ 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 is blacklisted.')
raise exceptions.AuthenticationFailed(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 is blacklisted.')
raise exceptions.AuthenticationFailed(msg)

user = self.authenticate_credentials(payload)

return (user, jwt_value)
Expand Down
Empty file.
18 changes: 18 additions & 0 deletions rest_framework_jwt/blacklist/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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:
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()
45 changes: 45 additions & 0 deletions rest_framework_jwt/blacklist/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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
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():
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really excited about introducing an optional requirement here just for a UUIDField. I think we can just simplify this for built-in Django 1.8 UUIDField and CharField for anything less.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good.. less dependency

"""
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)
1 change: 0 additions & 1 deletion rest_framework_jwt/models.py

This file was deleted.

Loading