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

Commit 4628b06

Browse files
author
Carlton Gibson
committed
Merge pull request #200 from willseward/master
Add support for private/public keys pairs
2 parents 6898b2c + f5a0d3b commit 4628b06

File tree

5 files changed

+145
-26
lines changed

5 files changed

+145
-26
lines changed

docs/index.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ JWT_AUTH = {
163163
'rest_framework_jwt.utils.jwt_response_payload_handler',
164164

165165
'JWT_SECRET_KEY': settings.SECRET_KEY,
166+
'JWT_PUBLIC_KEY': None,
167+
'JWT_PRIVATE_KEY': None,
166168
'JWT_ALGORITHM': 'HS256',
167169
'JWT_VERIFY': True,
168170
'JWT_VERIFY_EXPIRATION': True,
@@ -184,6 +186,16 @@ This is the secret key used to sign the JWT. Make sure this is safe and not shar
184186

185187
Default is your project's `settings.SECRET_KEY`.
186188

189+
### JWT_PUBLIC_KEY
190+
This is an object of type `cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`. It will be used to verify the signature of the incoming JWT. Will override `JWT_SECRET_KEY` when set. Read the [documentation](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey) for more details. Please note that `JWT_ALGORITHM` must be set to one of `RS256`, `RS384`, or `RS512`.
191+
192+
Default is `None`.
193+
194+
### JWT_PRIVATE_KEY
195+
This is an object of type `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`. It will be used to sign the signature component of the JWT. Will override `JWT_SECRET_KEY` when set. Read the [documentation](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey) for more details. Please note that `JWT_ALGORITHM` must be set to one of `RS256`, `RS384`, or `RS512`.
196+
197+
Default is `None`.
198+
187199
### JWT_ALGORITHM
188200

189201
Possible values are any of the [supported algorithms](https://github.com/jpadilla/pyjwt#algorithms) for cryptographic signing in PyJWT.

requirements/testing.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ pytest-django==2.8.0
44
pytest-cov==1.6
55

66
# Mocking the datetime module.
7-
freezegun==0.3.2
7+
cryptography==1.2.2

rest_framework_jwt/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
'JWT_PAYLOAD_GET_USER_ID_HANDLER':
2020
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',
2121

22+
'JWT_PRIVATE_KEY':
23+
None,
24+
25+
'JWT_PUBLIC_KEY':
26+
None,
27+
2228
'JWT_PAYLOAD_GET_USERNAME_HANDLER':
2329
'rest_framework_jwt.utils.jwt_get_username_from_payload_handler',
2430

rest_framework_jwt/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def jwt_get_username_from_payload_handler(payload):
6868
def jwt_encode_handler(payload):
6969
return jwt.encode(
7070
payload,
71-
api_settings.JWT_SECRET_KEY,
71+
api_settings.JWT_PRIVATE_KEY or api_settings.JWT_SECRET_KEY,
7272
api_settings.JWT_ALGORITHM
7373
).decode('utf-8')
7474

@@ -80,7 +80,7 @@ def jwt_decode_handler(token):
8080

8181
return jwt.decode(
8282
token,
83-
api_settings.JWT_SECRET_KEY,
83+
api_settings.JWT_PUBLIC_KEY or api_settings.JWT_SECRET_KEY,
8484
api_settings.JWT_VERIFY,
8585
options=options,
8686
leeway=api_settings.JWT_LEEWAY,

tests/test_views.py

Lines changed: 124 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import unittest
22
from calendar import timegm
33
from datetime import datetime, timedelta
4+
import time
45

56
from django import get_version
67
from django.test import TestCase
78
from django.test.utils import override_settings
89
from django.conf.urls import patterns
9-
from freezegun import freeze_time
1010
from rest_framework import status
1111
from rest_framework.test import APIClient
1212

1313
from rest_framework_jwt import utils
1414
from rest_framework_jwt.compat import get_user_model
1515
from rest_framework_jwt.settings import api_settings, DEFAULTS
1616

17+
from cryptography.hazmat.backends import default_backend
18+
from cryptography.hazmat.primitives.asymmetric import rsa
19+
1720
from . import utils as test_utils
1821

1922
User = get_user_model()
@@ -255,6 +258,9 @@ class TokenTestCase(BaseTestCase):
255258
Handlers for getting tokens from the API, or creating arbitrary ones.
256259
"""
257260

261+
def setUp(self):
262+
super(TokenTestCase, self).setUp()
263+
258264
def get_token(self):
259265
client = APIClient(enforce_csrf_checks=True)
260266
response = client.post('/auth-token/', self.data, format='json')
@@ -272,22 +278,20 @@ def create_token(self, user, exp=None, orig_iat=None):
272278
return token
273279

274280

275-
class VerifyJSONWebTokenTests(TokenTestCase):
281+
class VerifyJSONWebTokenTestsSymmetric(TokenTestCase):
276282

277283
def test_verify_jwt(self):
278284
"""
279285
Test that a valid, non-expired token will return a 200 response
280286
and itself when passed to the validation endpoint.
281287
"""
282288
client = APIClient(enforce_csrf_checks=True)
289+
orig_token = self.get_token()
283290

284-
with freeze_time('2015-01-01 00:00:01'):
285-
orig_token = self.get_token()
291+
# Now try to get a refreshed token
292+
response = client.post('/auth-token-verify/', {'token': orig_token},
293+
format='json')
286294

287-
with freeze_time('2015-01-01 00:00:10'):
288-
# Now try to get a refreshed token
289-
response = client.post('/auth-token-verify/', {'token': orig_token},
290-
format='json')
291295
self.assertEqual(response.status_code, status.HTTP_200_OK)
292296

293297
self.assertEqual(response.data['token'], orig_token)
@@ -345,6 +349,102 @@ def test_verify_jwt_fails_with_missing_user(self):
345349
"User doesn't exist")
346350

347351

352+
class VerifyJSONWebTokenTestsAsymmetric(TokenTestCase):
353+
354+
def setUp(self):
355+
356+
super(VerifyJSONWebTokenTestsAsymmetric, self).setUp()
357+
358+
private_key = rsa.generate_private_key(public_exponent=65537,
359+
key_size=2048,
360+
backend=default_backend())
361+
public_key = private_key.public_key()
362+
363+
api_settings.JWT_PRIVATE_KEY = private_key
364+
api_settings.JWT_PUBLIC_KEY = public_key
365+
api_settings.JWT_ALGORITHM = 'RS512'
366+
367+
def test_verify_jwt_with_pub_pvt_key(self):
368+
"""
369+
Test that a token can be signed with asymmetrics keys
370+
"""
371+
client = APIClient(enforce_csrf_checks=True)
372+
373+
orig_token = self.get_token()
374+
375+
# Now try to get a refreshed token
376+
response = client.post('/auth-token-verify/', {'token': orig_token},
377+
format='json')
378+
379+
self.assertEqual(response.status_code, status.HTTP_200_OK)
380+
self.assertEqual(response.data['token'], orig_token)
381+
382+
def test_verify_jwt_fails_with_expired_token(self):
383+
"""
384+
Test that an expired token will fail with the correct error.
385+
"""
386+
client = APIClient(enforce_csrf_checks=True)
387+
388+
# Make an expired token..
389+
token = self.create_token(
390+
self.user,
391+
exp=datetime.utcnow() - timedelta(seconds=5),
392+
orig_iat=datetime.utcnow() - timedelta(hours=1)
393+
)
394+
395+
response = client.post('/auth-token-verify/', {'token': token},
396+
format='json')
397+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
398+
self.assertRegexpMatches(response.data['non_field_errors'][0],
399+
'Signature has expired')
400+
401+
def test_verify_jwt_fails_with_bad_token(self):
402+
"""
403+
Test that an invalid token will fail with the correct error.
404+
"""
405+
406+
client = APIClient(enforce_csrf_checks=True)
407+
408+
token = "i am not a correctly formed token"
409+
410+
response = client.post('/auth-token-verify/', {'token': token},
411+
format='json')
412+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
413+
self.assertRegexpMatches(response.data['non_field_errors'][0],
414+
'Error decoding signature')
415+
416+
def test_verify_jwt_fails_with_bad_pvt_key(self):
417+
"""
418+
Test that an mismatched private key token will fail with
419+
the correct error.
420+
"""
421+
422+
# Generate a new private key
423+
private_key = rsa.generate_private_key(public_exponent=65537,
424+
key_size=2048,
425+
backend=default_backend())
426+
427+
# Don't set the private key
428+
api_settings.JWT_PRIVATE_KEY = private_key
429+
430+
client = APIClient(enforce_csrf_checks=True)
431+
orig_token = self.get_token()
432+
433+
# Now try to get a refreshed token
434+
response = client.post('/auth-token-verify/', {'token': orig_token},
435+
format='json')
436+
437+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
438+
self.assertRegexpMatches(response.data['non_field_errors'][0],
439+
'Error decoding signature')
440+
441+
def tearDown(self):
442+
# Restore original settings
443+
api_settings.JWT_ALGORITHM = DEFAULTS['JWT_ALGORITHM']
444+
api_settings.JWT_PRIVATE_KEY = DEFAULTS['JWT_PRIVATE_KEY']
445+
api_settings.JWT_PUBLIC_KEY = DEFAULTS['JWT_PUBLIC_KEY']
446+
447+
348448
class RefreshJSONWebTokenTests(TokenTestCase):
349449

350450
def setUp(self):
@@ -354,28 +454,29 @@ def setUp(self):
354454
def test_refresh_jwt(self):
355455
"""
356456
Test getting a refreshed token from original token works
457+
458+
No date/time modifications are neccessary because it is assumed
459+
that this operation will take less than 300 seconds.
357460
"""
358461
client = APIClient(enforce_csrf_checks=True)
462+
orig_token = self.get_token()
463+
orig_token_decoded = utils.jwt_decode_handler(orig_token)
359464

360-
with freeze_time('2015-01-01 00:00:01'):
361-
orig_token = self.get_token()
362-
orig_token_decoded = utils.jwt_decode_handler(orig_token)
363-
364-
expected_orig_iat = timegm(datetime.utcnow().utctimetuple())
465+
expected_orig_iat = timegm(datetime.utcnow().utctimetuple())
365466

366-
# Make sure 'orig_iat' exists and is the current time (give some slack)
367-
orig_iat = orig_token_decoded['orig_iat']
368-
self.assertLessEqual(orig_iat - expected_orig_iat, 1)
467+
# Make sure 'orig_iat' exists and is the current time (give some slack)
468+
orig_iat = orig_token_decoded['orig_iat']
469+
self.assertLessEqual(orig_iat - expected_orig_iat, 1)
369470

370-
with freeze_time('2015-01-01 00:00:03'):
471+
time.sleep(1)
371472

372-
# Now try to get a refreshed token
373-
response = client.post('/auth-token-refresh/', {'token': orig_token},
374-
format='json')
375-
self.assertEqual(response.status_code, status.HTTP_200_OK)
473+
# Now try to get a refreshed token
474+
response = client.post('/auth-token-refresh/', {'token': orig_token},
475+
format='json')
476+
self.assertEqual(response.status_code, status.HTTP_200_OK)
376477

377-
new_token = response.data['token']
378-
new_token_decoded = utils.jwt_decode_handler(new_token)
478+
new_token = response.data['token']
479+
new_token_decoded = utils.jwt_decode_handler(new_token)
379480

380481
# Make sure 'orig_iat' on the new token is same as original
381482
self.assertEquals(new_token_decoded['orig_iat'], orig_iat)

0 commit comments

Comments
 (0)