Skip to content

Commit a775004

Browse files
Revoke access token if user password is changed (#719)
1 parent d2cd59d commit a775004

File tree

6 files changed

+86
-1
lines changed

6 files changed

+86
-1
lines changed

docs/settings.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,18 @@ More about this in the "Sliding tokens" section below.
272272

273273
The claim name that is used to store the expiration time of a sliding token's
274274
refresh period. More about this in the "Sliding tokens" section below.
275+
276+
``CHECK_REVOKE_TOKEN``
277+
--------------------
278+
279+
If this field is set to ``True``, the system will verify whether the token
280+
has been revoked or not by comparing the md5 hash of the user's current
281+
password with the value stored in the REVOKE_TOKEN_CLAIM field within the
282+
payload of the JWT token.
283+
284+
``REVOKE_TOKEN_CLAIM``
285+
--------------------
286+
287+
The claim name that is used to store a user hash password.
288+
If the value of this CHECK_REVOKE_TOKEN field is ``True``, this field will be
289+
included in the JWT payload.

rest_framework_simplejwt/authentication.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .models import TokenUser
1111
from .settings import api_settings
1212
from .tokens import Token
13+
from .utils import get_md5_hash_password
1314

1415
AUTH_HEADER_TYPES = api_settings.AUTH_HEADER_TYPES
1516

@@ -133,6 +134,14 @@ def get_user(self, validated_token: Token) -> AuthUser:
133134
if not user.is_active:
134135
raise AuthenticationFailed(_("User is inactive"), code="user_inactive")
135136

137+
if api_settings.CHECK_REVOKE_TOKEN:
138+
if validated_token.get(
139+
api_settings.REVOKE_TOKEN_CLAIM
140+
) != get_md5_hash_password(user.password):
141+
raise AuthenticationFailed(
142+
_("The user's password has been changed."), code="password_changed"
143+
)
144+
136145
return user
137146

138147

rest_framework_simplejwt/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
4343
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
4444
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
45+
"CHECK_REVOKE_TOKEN": False,
46+
"REVOKE_TOKEN_CLAIM": "hash_password",
4547
}
4648

4749
IMPORT_STRINGS = (

rest_framework_simplejwt/tokens.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111
from .models import TokenUser
1212
from .settings import api_settings
1313
from .token_blacklist.models import BlacklistedToken, OutstandingToken
14-
from .utils import aware_utcnow, datetime_from_epoch, datetime_to_epoch, format_lazy
14+
from .utils import (
15+
aware_utcnow,
16+
datetime_from_epoch,
17+
datetime_to_epoch,
18+
format_lazy,
19+
get_md5_hash_password,
20+
)
1521

1622
if TYPE_CHECKING:
1723
from .backends import TokenBackend
@@ -201,6 +207,11 @@ def for_user(cls, user: AuthUser) -> "Token":
201207
token = cls()
202208
token[api_settings.USER_ID_CLAIM] = user_id
203209

210+
if api_settings.CHECK_REVOKE_TOKEN:
211+
token[api_settings.REVOKE_TOKEN_CLAIM] = get_md5_hash_password(
212+
user.password
213+
)
214+
204215
return token
205216

206217
_token_backend: Optional["TokenBackend"] = None

rest_framework_simplejwt/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
from calendar import timegm
23
from datetime import datetime, timezone
34
from typing import Callable
@@ -7,6 +8,13 @@
78
from django.utils.timezone import is_naive, make_aware
89

910

11+
def get_md5_hash_password(password: str) -> str:
12+
"""
13+
Returns MD5 hash of the given password
14+
"""
15+
return hashlib.md5(password.encode()).hexdigest().upper()
16+
17+
1018
def make_utc(dt: datetime) -> datetime:
1119
if settings.USE_TZ and is_naive(dt):
1220
return make_aware(dt, timezone=timezone.utc)

tests/test_authentication.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from rest_framework_simplejwt.models import TokenUser
1111
from rest_framework_simplejwt.settings import api_settings
1212
from rest_framework_simplejwt.tokens import AccessToken, SlidingToken
13+
from rest_framework_simplejwt.utils import get_md5_hash_password
1314

1415
from .utils import override_api_settings
1516

@@ -160,6 +161,45 @@ def test_get_user(self):
160161
# Otherwise, should return correct user
161162
self.assertEqual(self.backend.get_user(payload).id, u.id)
162163

164+
@override_api_settings(
165+
CHECK_REVOKE_TOKEN=True, REVOKE_TOKEN_CLAIM="revoke_token_claim"
166+
)
167+
def test_get_user_with_check_revoke_token(self):
168+
payload = {"some_other_id": "foo"}
169+
170+
# Should raise error if no recognizable user identification
171+
with self.assertRaises(InvalidToken):
172+
self.backend.get_user(payload)
173+
174+
payload[api_settings.USER_ID_CLAIM] = 42
175+
176+
# Should raise exception if user not found
177+
with self.assertRaises(AuthenticationFailed):
178+
self.backend.get_user(payload)
179+
180+
u = User.objects.create_user(username="markhamill")
181+
u.is_active = False
182+
u.save()
183+
184+
payload[api_settings.USER_ID_CLAIM] = getattr(u, api_settings.USER_ID_FIELD)
185+
186+
# Should raise exception if user is inactive
187+
with self.assertRaises(AuthenticationFailed):
188+
self.backend.get_user(payload)
189+
190+
u.is_active = True
191+
u.save()
192+
193+
# Should raise exception if hash password is different
194+
with self.assertRaises(AuthenticationFailed):
195+
self.backend.get_user(payload)
196+
197+
if api_settings.CHECK_REVOKE_TOKEN:
198+
payload[api_settings.REVOKE_TOKEN_CLAIM] = get_md5_hash_password(u.password)
199+
200+
# Otherwise, should return correct user
201+
self.assertEqual(self.backend.get_user(payload).id, u.id)
202+
163203

164204
class TestJWTStatelessUserAuthentication(TestCase):
165205
def setUp(self):

0 commit comments

Comments
 (0)