Skip to content

Commit b63dc86

Browse files
committed
Check for blocked tokens depending on the setting for token ids.
If all valid tokens should have a token id ('require'), then all still-valid blacklisted tokens should have been recorded with a token_id, and we can purely look up using the id. Since the migration adding this column backfills the token_id values, this should be safe to rely on immediately after the migration completes. If tokens are being generated without ids ('off'), then we look up the blacklist record by the whole token value as before. This means that when altering the config from 'require' to 'off' existing blacklisted tokens would no longer match. I doubt that is going to be a common transition in practice. If someone needed to do that carefully, they could shift from 'require' to 'include' for one token expiration horizon, and then to 'off', but I think this is sufficiently fringe to ignore in practice. For the default setting ('include'), we check for a blacklist entry with either the token id or the exact token value. This should ensure that for people who have kept the defaults, things work in as straightforward manner as possible. Refreshed tokens are invalidated if any token from the same chain of refreshes is invalidated, which still improves logout security for the default case, even though we still storing the full token values, which has some downsides.
1 parent e0b0ed5 commit b63dc86

File tree

4 files changed

+25
-5
lines changed

4 files changed

+25
-5
lines changed

src/rest_framework_jwt/authentication.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def authenticate(self, request):
8282

8383
if apps.is_installed('rest_framework_jwt.blacklist'):
8484
from rest_framework_jwt.blacklist.models import BlacklistedToken
85-
if BlacklistedToken.is_blocked(token):
85+
if BlacklistedToken.is_blocked(token, payload):
8686
msg = _('Token is blacklisted.')
8787
raise exceptions.PermissionDenied(msg)
8888

src/rest_framework_jwt/blacklist/models.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from django.utils import timezone
88
from django.utils.encoding import force_str
99

10+
from rest_framework_jwt.settings import api_settings
11+
1012

1113
class BlacklistedTokenManager(models.Manager):
1214
def delete_stale_tokens(self):
@@ -38,5 +40,19 @@ def __str__(self):
3840

3941

4042
@staticmethod
41-
def is_blocked(token):
42-
return BlacklistedToken.objects.filter(token=force_str(token)).exists()
43+
def is_blocked(token, payload):
44+
token = force_str(token)
45+
46+
# For invalidated tokens that have an original token id (orig_jti),
47+
# we record that in the list, so that the whole family of tokens
48+
# refreshed from the same initial token is rejected.
49+
token_id = payload.get('orig_jti') or payload.get('jti')
50+
51+
if api_settings.JWT_TOKEN_ID == 'require':
52+
query = Q(token_id=token_id)
53+
elif api_settings.JWT_TOKEN_ID == 'off':
54+
query = Q(token=token)
55+
else:
56+
query = Q(token__isnull=False, token=token) | Q(token_id__isnull=False, token_id=token_id)
57+
58+
return BlacklistedToken.objects.filter(query).exists()

src/rest_framework_jwt/blacklist/permissions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import jwt
2+
13
from rest_framework.permissions import BasePermission
24

35
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
@@ -15,4 +17,6 @@ def has_permission(self, request, view):
1517
if token is None:
1618
return True
1719

18-
return not BlacklistedToken.is_blocked(token)
20+
# The token should already be validated before we call this.
21+
payload = jwt.decode(token, None, False)
22+
return not BlacklistedToken.is_blocked(token, payload)

src/rest_framework_jwt/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def check_payload(token):
217217

218218
if apps.is_installed('rest_framework_jwt.blacklist'):
219219
from rest_framework_jwt.blacklist.models import BlacklistedToken
220-
if BlacklistedToken.is_blocked(token):
220+
if BlacklistedToken.is_blocked(token, payload):
221221
msg = _('Token is blacklisted.')
222222
raise serializers.ValidationError(msg)
223223

0 commit comments

Comments
 (0)