Skip to content
This repository was archived by the owner on May 26, 2020. It is now read-only.

Commit faa772c

Browse files
committed
Merge pull request #75 from TrackMaven/jwt-verification-view
Added a JWT verification view
2 parents 94f0a1c + 0f2993c commit faa772c

File tree

4 files changed

+166
-54
lines changed

4 files changed

+166
-54
lines changed

docs/index.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain
101101

102102
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.
103103

104+
## Verify Token
105+
106+
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.
107+
108+
This setup is supported in this package using a verification endpoint. Add the following URL pattern:
109+
```python
110+
url(r'^api-token-verify/', 'rest_framework_jwt.views.verify_jwt_token'),
111+
```
112+
113+
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.
114+
115+
```bash
116+
$ curl -X POST -H "Content-Type: application/json" -d '{"token":"<EXISTING_TOKEN>"}' http://localhost:8000/api-token-verify/
117+
```
118+
104119
## Additional Settings
105120
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.
106121

rest_framework_jwt/serializers.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,17 @@ def validate(self, attrs):
8181
raise serializers.ValidationError(msg)
8282

8383

84-
class RefreshJSONWebTokenSerializer(Serializer):
84+
class VerificationBaseSerializer(Serializer):
8585
"""
86-
Check an access token
86+
Abstract serializer used for verifying and refreshing JWTs.
8787
"""
8888
token = serializers.CharField()
8989

9090
def validate(self, attrs):
91-
User = utils.get_user_model()
92-
token = attrs['token']
91+
msg = 'Please define a validate method.'
92+
raise NotImplementedError(msg)
9393

94+
def _check_payload(self, token):
9495
# Check payload valid (based off of JSONWebTokenAuthentication,
9596
# may want to refactor)
9697
try:
@@ -102,6 +103,10 @@ def validate(self, attrs):
102103
msg = _('Error decoding signature.')
103104
raise serializers.ValidationError(msg)
104105

106+
return payload
107+
108+
def _check_user(self, payload):
109+
User = utils.get_user_model()
105110
# Make sure user exists (may want to refactor this)
106111
try:
107112
user_id = jwt_get_user_id_from_payload(payload)
@@ -115,6 +120,38 @@ def validate(self, attrs):
115120
msg = _("User doesn't exist.")
116121
raise serializers.ValidationError(msg)
117122

123+
return user
124+
125+
126+
class VerifyJSONWebTokenSerializer(VerificationBaseSerializer):
127+
"""
128+
Check the veracity of an access token.
129+
"""
130+
131+
def validate(self, attrs):
132+
token = attrs['token']
133+
134+
payload = self._check_payload(token=token)
135+
user = self._check_user(payload=payload)
136+
137+
new_payload = jwt_payload_handler(user)
138+
139+
return {
140+
'token': jwt_encode_handler(new_payload),
141+
'user': user
142+
}
143+
144+
145+
class RefreshJSONWebTokenSerializer(VerificationBaseSerializer):
146+
"""
147+
Refresh an access token.
148+
"""
149+
150+
def validate(self, attrs):
151+
token = attrs['token']
152+
153+
payload = self._check_payload(token=token)
154+
user = self._check_user(payload=payload)
118155
# Get and check 'orig_iat'
119156
orig_iat = payload.get('orig_iat')
120157

rest_framework_jwt/views.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@
66

77
from rest_framework_jwt.settings import api_settings
88

9-
from .serializers import JSONWebTokenSerializer, RefreshJSONWebTokenSerializer
9+
from .serializers import (
10+
JSONWebTokenSerializer, RefreshJSONWebTokenSerializer,
11+
VerifyJSONWebTokenSerializer
12+
)
1013

1114
jwt_response_payload_handler = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER
1215

1316

14-
class ObtainJSONWebToken(APIView):
17+
class JSONWebTokenAPIView(APIView):
1518
"""
16-
API View that receives a POST with a user's username and password.
17-
18-
Returns a JSON Web Token that can be used for authenticated requests.
19+
Base API View that various JWT interactions inherit from.
1920
"""
2021
throttle_classes = ()
2122
permission_classes = ()
2223
authentication_classes = ()
2324
parser_classes = (parsers.FormParser, parsers.JSONParser,)
2425
renderer_classes = (renderers.JSONRenderer,)
25-
serializer_class = JSONWebTokenSerializer
2626

2727
def post(self, request):
2828
serializer = self.serializer_class(data=request.DATA)
@@ -37,33 +37,34 @@ def post(self, request):
3737
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
3838

3939

40-
class RefreshJSONWebToken(APIView):
40+
class ObtainJSONWebToken(JSONWebTokenAPIView):
41+
"""
42+
API View that receives a POST with a user's username and password.
43+
44+
Returns a JSON Web Token that can be used for authenticated requests.
45+
"""
46+
serializer_class = JSONWebTokenSerializer
47+
48+
49+
class VerifyJSONWebToken(JSONWebTokenAPIView):
50+
"""
51+
API View that checks the veracity of a token, returning the token if it
52+
is valid.
53+
"""
54+
serializer_class = VerifyJSONWebTokenSerializer
55+
56+
57+
class RefreshJSONWebToken(JSONWebTokenAPIView):
4158
"""
4259
API View that returns a refreshed token (with new expiration) based on
4360
existing token
4461
4562
If 'orig_iat' field (original issued-at-time) is found, will first check
4663
if it's within expiration window, then copy it to the new token
4764
"""
48-
throttle_classes = ()
49-
permission_classes = ()
50-
authentication_classes = ()
51-
parser_classes = (parsers.FormParser, parsers.JSONParser,)
52-
renderer_classes = (renderers.JSONRenderer,)
5365
serializer_class = RefreshJSONWebTokenSerializer
5466

55-
def post(self, request):
56-
serializer = self.serializer_class(data=request.DATA)
57-
58-
if serializer.is_valid():
59-
user = serializer.object.get('user') or request.user
60-
token = serializer.object.get('token')
61-
response_data = jwt_response_payload_handler(token, user, request)
62-
63-
return Response(response_data)
64-
65-
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
66-
6767

6868
obtain_jwt_token = ObtainJSONWebToken.as_view()
6969
refresh_jwt_token = RefreshJSONWebToken.as_view()
70+
verify_jwt_token = VerifyJSONWebToken.as_view()

tests/test_views.py

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
'',
2626
(r'^auth-token/$', 'rest_framework_jwt.views.obtain_jwt_token'),
2727
(r'^auth-token-refresh/$', 'rest_framework_jwt.views.refresh_jwt_token'),
28+
(r'^auth-token-verify/$', 'rest_framework_jwt.views.verify_jwt_token'),
29+
2830
)
2931

3032
orig_datetime = datetime
@@ -204,20 +206,18 @@ def test_jwt_login_json_bad_creds(self):
204206
self.assertEqual(response.status_code, 400)
205207

206208

207-
class RefreshJSONWebTokenTests(BaseTestCase):
208-
urls = 'tests.test_views'
209-
210-
def setUp(self):
211-
super(RefreshJSONWebTokenTests, self).setUp()
212-
api_settings.JWT_ALLOW_REFRESH = True
209+
class TokenTestCase(BaseTestCase):
210+
"""
211+
Handlers for getting tokens from the API, or creating arbitrary ones.
212+
"""
213213

214214
def get_token(self):
215215
client = APIClient(enforce_csrf_checks=True)
216216
response = client.post('/auth-token/', self.data, format='json')
217217
return response.data['token']
218218

219219
def create_token(self, user, exp=None, orig_iat=None):
220-
payload = utils.jwt_payload_handler(self.user)
220+
payload = utils.jwt_payload_handler(user)
221221
if exp:
222222
payload['exp'] = exp
223223

@@ -227,6 +227,84 @@ def create_token(self, user, exp=None, orig_iat=None):
227227
token = utils.jwt_encode_handler(payload)
228228
return token
229229

230+
231+
class VerifyJSONWebTokenTests(TokenTestCase):
232+
233+
def test_verify_jwt(self):
234+
"""
235+
Test that a valid, non-expired token will return a 200 response
236+
and itself when passed to the validation endpoint.
237+
"""
238+
client = APIClient(enforce_csrf_checks=True)
239+
240+
orig_token = self.get_token()
241+
242+
# Now try to get a refreshed token
243+
response = client.post('/auth-token-verify/', {'token': orig_token},
244+
format='json')
245+
self.assertEqual(response.status_code, status.HTTP_200_OK)
246+
247+
self.assertEqual(response.data['token'], orig_token)
248+
249+
def test_verify_jwt_fails_with_expired_token(self):
250+
"""
251+
Test that an expired token will fail with the correct error.
252+
"""
253+
client = APIClient(enforce_csrf_checks=True)
254+
255+
# Make an expired token..
256+
token = self.create_token(
257+
self.user,
258+
exp=datetime.utcnow() - timedelta(seconds=5),
259+
orig_iat=datetime.utcnow() - timedelta(hours=1)
260+
)
261+
262+
response = client.post('/auth-token-verify/', {'token': token},
263+
format='json')
264+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
265+
self.assertRegexpMatches(response.data['non_field_errors'][0],
266+
'Signature has expired')
267+
268+
def test_verify_jwt_fails_with_bad_token(self):
269+
"""
270+
Test that an invalid token will fail with the correct error.
271+
"""
272+
client = APIClient(enforce_csrf_checks=True)
273+
274+
token = "i am not a correctly formed token"
275+
276+
response = client.post('/auth-token-verify/', {'token': token},
277+
format='json')
278+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
279+
self.assertRegexpMatches(response.data['non_field_errors'][0],
280+
'Error decoding signature')
281+
282+
def test_verify_jwt_fails_with_missing_user(self):
283+
"""
284+
Test that an invalid token will fail with a user that does not exist.
285+
"""
286+
client = APIClient(enforce_csrf_checks=True)
287+
288+
user = User.objects.create_user(
289+
email='jsmith@example.com', username='jsmith', password='password')
290+
291+
token = self.create_token(user)
292+
# Delete the user used to make the token
293+
user.delete()
294+
295+
response = client.post('/auth-token-verify/', {'token': token},
296+
format='json')
297+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
298+
self.assertRegexpMatches(response.data['non_field_errors'][0],
299+
"User doesn't exist")
300+
301+
302+
class RefreshJSONWebTokenTests(TokenTestCase):
303+
304+
def setUp(self):
305+
super(RefreshJSONWebTokenTests, self).setUp()
306+
api_settings.JWT_ALLOW_REFRESH = True
307+
230308
def test_refresh_jwt(self):
231309
"""
232310
Test getting a refreshed token from original token works
@@ -257,25 +335,6 @@ def test_refresh_jwt(self):
257335
self.assertEquals(new_token_decoded['orig_iat'], orig_iat)
258336
self.assertGreater(new_token_decoded['exp'], orig_token_decoded['exp'])
259337

260-
def test_refresh_jwt_fails_with_expired_token(self):
261-
"""
262-
Test that using an expired token to refresh won't work
263-
"""
264-
client = APIClient(enforce_csrf_checks=True)
265-
266-
# Make an expired token..
267-
token = self.create_token(
268-
self.user,
269-
exp=datetime.utcnow() - timedelta(seconds=5),
270-
orig_iat=datetime.utcnow() - timedelta(hours=1)
271-
)
272-
273-
response = client.post('/auth-token-refresh/', {'token': token},
274-
format='json')
275-
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
276-
self.assertRegexpMatches(response.data['non_field_errors'][0],
277-
'Signature has expired')
278-
279338
def test_refresh_jwt_after_refresh_expiration(self):
280339
"""
281340
Test that token can't be refreshed after token refresh limit

0 commit comments

Comments
 (0)