From 9e905fc8e47ab360fb9337f93f1f4463bc60068b Mon Sep 17 00:00:00 2001 From: alichass Date: Sun, 14 Jun 2020 17:26:28 -0400 Subject: [PATCH 1/5] added the ability to customise claims in the jwt token - has tests JWT claim serializer now can be set to something custom in settings: JWT_TOKEN_CLAIMS_SERIALIZER = myTokenObtainSerializer Ideally JWT_TOKEN_CLAIMS_SERIALIZER would be a key in REST_AUTH_SERIALIZERS and assigned through import_callable, as with the other serializers; however, I could not quite figure out how to implement it that way --- dj_rest_auth/tests/test_api.py | 66 ++++++++++++++++++++++++++++++++++ dj_rest_auth/utils.py | 20 +++++------ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/dj_rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py index 2115415..9554d05 100644 --- a/dj_rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -18,6 +18,9 @@ try: except ImportError: from django.core.urlresolvers import reverse +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from jwt import decode as decode_jwt + @override_settings(ROOT_URLCONF="tests.urls") class APIBasicTests(TestsMixin, TestCase): @@ -605,3 +608,66 @@ class APIBasicTests(TestsMixin, TestCase): # test other TokenError, AttributeError, TypeError (invalid format) resp = self.post(self.logout_url, status=200, data=json.dumps({'refresh': token})) self.assertEqual(resp.status_code, 500) + + + + class TESTTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + # Add custom claims + token['name'] = user.username + token['email'] = user.email + + return token + + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE=None) + @override_settings(REST_FRAMEWORK=dict( + DEFAULT_AUTHENTICATION_CLASSES=[ + 'dj_rest_auth.utils.JWTCookieAuthentication' + ] + )) + @override_settings(REST_SESSION_LOGIN=False) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + def test_custom_jwt_claims(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('access_token' in self.response.json.keys(), True) + self.token = self.response.json['access_token'] + claims = decode_jwt(self.token, settings.SECRET_KEY, algorithms='HS256') + self.assertEquals(claims['user_id'], 1) + self.assertEquals(claims['name'], 'person') + self.assertEquals(claims['email'], 'person1@world.com') + + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE='jwt-auth') + @override_settings(REST_FRAMEWORK=dict( + DEFAULT_AUTHENTICATION_CLASSES=[ + 'dj_rest_auth.utils.JWTCookieAuthentication' + ] + )) + @override_settings(REST_SESSION_LOGIN=False) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + def test_custom_jwt_claims_cookie_w_authentication(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + self.assertEqual(['jwt-auth'], list(resp.cookies.keys())) + token = resp.cookies.get('jwt-auth').value + claims = decode_jwt(token, settings.SECRET_KEY, algorithms='HS256') + self.assertEquals(claims['user_id'], 1) + self.assertEquals(claims['name'], 'person') + self.assertEquals(claims['email'], 'person1@world.com') + resp = self.get('/protected-view/') + self.assertEquals(resp.status_code, 200) \ No newline at end of file diff --git a/dj_rest_auth/utils.py b/dj_rest_auth/utils.py index e1ef77a..5f229af 100644 --- a/dj_rest_auth/utils.py +++ b/dj_rest_auth/utils.py @@ -15,18 +15,15 @@ def default_create_token(token_model, user, serializer): return token -def jwt_encode(user): - try: - from rest_framework_simplejwt.serializers import TokenObtainPairSerializer - except ImportError: - raise ImportError("rest-framework-simplejwt needs to be installed") - - refresh = TokenObtainPairSerializer.get_token(user) - return refresh.access_token, refresh - - try: + from django.conf import settings from rest_framework_simplejwt.authentication import JWTAuthentication + from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + + def jwt_encode(user): + TOPS = getattr(settings, 'JWT_TOKEN_CLAIMS_SERIALIZER', TokenObtainPairSerializer) + refresh = TOPS.get_token(user) + return refresh.access_token, refresh class JWTCookieAuthentication(JWTAuthentication): """ @@ -35,7 +32,6 @@ try: preference to the header). """ def authenticate(self, request): - from django.conf import settings cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None) header = self.get_header(request) if header is None: @@ -53,4 +49,4 @@ try: return self.get_user(validated_token), validated_token except ImportError: - pass + raise ImportError("rest-framework-simplejwt needs to be installed") From 15ad7f4c737a63545901e500e2e2b93fac3864a9 Mon Sep 17 00:00:00 2001 From: alichass Date: Sun, 14 Jun 2020 19:15:22 -0400 Subject: [PATCH 2/5] made JWT_TOKEN_CLAIMS_SERIALIZER setting value a callable string rather than a function --- dj_rest_auth/tests/test_api.py | 26 ++++++++++++-------------- dj_rest_auth/utils.py | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/dj_rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py index 9554d05..f25f5fc 100644 --- a/dj_rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -21,6 +21,16 @@ except ImportError: from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from jwt import decode as decode_jwt +class TESTTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + # Add custom claims + token['name'] = user.username + token['email'] = user.email + + return token + @override_settings(ROOT_URLCONF="tests.urls") class APIBasicTests(TestsMixin, TestCase): @@ -610,18 +620,6 @@ class APIBasicTests(TestsMixin, TestCase): self.assertEqual(resp.status_code, 500) - - class TESTTokenObtainPairSerializer(TokenObtainPairSerializer): - @classmethod - def get_token(cls, user): - token = super().get_token(user) - # Add custom claims - token['name'] = user.username - token['email'] = user.email - - return token - - @override_settings(REST_USE_JWT=True) @override_settings(JWT_AUTH_COOKIE=None) @override_settings(REST_FRAMEWORK=dict( @@ -630,7 +628,7 @@ class APIBasicTests(TestsMixin, TestCase): ] )) @override_settings(REST_SESSION_LOGIN=False) - @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = 'tests.test_api.TESTTokenObtainPairSerializer') def test_custom_jwt_claims(self): payload = { "username": self.USERNAME, @@ -655,7 +653,7 @@ class APIBasicTests(TestsMixin, TestCase): ] )) @override_settings(REST_SESSION_LOGIN=False) - @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = 'tests.test_api.TESTTokenObtainPairSerializer') def test_custom_jwt_claims_cookie_w_authentication(self): payload = { "username": self.USERNAME, diff --git a/dj_rest_auth/utils.py b/dj_rest_auth/utils.py index 5f229af..fa971de 100644 --- a/dj_rest_auth/utils.py +++ b/dj_rest_auth/utils.py @@ -21,7 +21,7 @@ try: from rest_framework_simplejwt.serializers import TokenObtainPairSerializer def jwt_encode(user): - TOPS = getattr(settings, 'JWT_TOKEN_CLAIMS_SERIALIZER', TokenObtainPairSerializer) + TOPS = import_callable(getattr(settings, 'JWT_TOKEN_CLAIMS_SERIALIZER', TokenObtainPairSerializer)) refresh = TOPS.get_token(user) return refresh.access_token, refresh From 0722ec4aeeefeae08a8e9a801394b4a09ee7670c Mon Sep 17 00:00:00 2001 From: alichass Date: Sun, 14 Jun 2020 17:26:28 -0400 Subject: [PATCH 3/5] added the ability to customise claims in the jwt token - has tests JWT claim serializer now can be set to something custom in settings: JWT_TOKEN_CLAIMS_SERIALIZER = myTokenObtainSerializer Ideally JWT_TOKEN_CLAIMS_SERIALIZER would be a key in REST_AUTH_SERIALIZERS and assigned through import_callable, as with the other serializers; however, I could not quite figure out how to implement it that way --- dj_rest_auth/tests/test_api.py | 66 ++++++++++++++++++++++++++++++++++ dj_rest_auth/utils.py | 20 +++++------ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/dj_rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py index 2115415..9554d05 100644 --- a/dj_rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -18,6 +18,9 @@ try: except ImportError: from django.core.urlresolvers import reverse +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from jwt import decode as decode_jwt + @override_settings(ROOT_URLCONF="tests.urls") class APIBasicTests(TestsMixin, TestCase): @@ -605,3 +608,66 @@ class APIBasicTests(TestsMixin, TestCase): # test other TokenError, AttributeError, TypeError (invalid format) resp = self.post(self.logout_url, status=200, data=json.dumps({'refresh': token})) self.assertEqual(resp.status_code, 500) + + + + class TESTTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + # Add custom claims + token['name'] = user.username + token['email'] = user.email + + return token + + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE=None) + @override_settings(REST_FRAMEWORK=dict( + DEFAULT_AUTHENTICATION_CLASSES=[ + 'dj_rest_auth.utils.JWTCookieAuthentication' + ] + )) + @override_settings(REST_SESSION_LOGIN=False) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + def test_custom_jwt_claims(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('access_token' in self.response.json.keys(), True) + self.token = self.response.json['access_token'] + claims = decode_jwt(self.token, settings.SECRET_KEY, algorithms='HS256') + self.assertEquals(claims['user_id'], 1) + self.assertEquals(claims['name'], 'person') + self.assertEquals(claims['email'], 'person1@world.com') + + + @override_settings(REST_USE_JWT=True) + @override_settings(JWT_AUTH_COOKIE='jwt-auth') + @override_settings(REST_FRAMEWORK=dict( + DEFAULT_AUTHENTICATION_CLASSES=[ + 'dj_rest_auth.utils.JWTCookieAuthentication' + ] + )) + @override_settings(REST_SESSION_LOGIN=False) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + def test_custom_jwt_claims_cookie_w_authentication(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + resp = self.post(self.login_url, data=payload, status_code=200) + self.assertEqual(['jwt-auth'], list(resp.cookies.keys())) + token = resp.cookies.get('jwt-auth').value + claims = decode_jwt(token, settings.SECRET_KEY, algorithms='HS256') + self.assertEquals(claims['user_id'], 1) + self.assertEquals(claims['name'], 'person') + self.assertEquals(claims['email'], 'person1@world.com') + resp = self.get('/protected-view/') + self.assertEquals(resp.status_code, 200) \ No newline at end of file diff --git a/dj_rest_auth/utils.py b/dj_rest_auth/utils.py index e1ef77a..5f229af 100644 --- a/dj_rest_auth/utils.py +++ b/dj_rest_auth/utils.py @@ -15,18 +15,15 @@ def default_create_token(token_model, user, serializer): return token -def jwt_encode(user): - try: - from rest_framework_simplejwt.serializers import TokenObtainPairSerializer - except ImportError: - raise ImportError("rest-framework-simplejwt needs to be installed") - - refresh = TokenObtainPairSerializer.get_token(user) - return refresh.access_token, refresh - - try: + from django.conf import settings from rest_framework_simplejwt.authentication import JWTAuthentication + from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + + def jwt_encode(user): + TOPS = getattr(settings, 'JWT_TOKEN_CLAIMS_SERIALIZER', TokenObtainPairSerializer) + refresh = TOPS.get_token(user) + return refresh.access_token, refresh class JWTCookieAuthentication(JWTAuthentication): """ @@ -35,7 +32,6 @@ try: preference to the header). """ def authenticate(self, request): - from django.conf import settings cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None) header = self.get_header(request) if header is None: @@ -53,4 +49,4 @@ try: return self.get_user(validated_token), validated_token except ImportError: - pass + raise ImportError("rest-framework-simplejwt needs to be installed") From 1dce78194241112f9b0829b26b710c5354248b5d Mon Sep 17 00:00:00 2001 From: alichass Date: Sun, 14 Jun 2020 19:15:22 -0400 Subject: [PATCH 4/5] made JWT_TOKEN_CLAIMS_SERIALIZER setting value a callable string rather than a function --- dj_rest_auth/tests/test_api.py | 26 ++++++++++++-------------- dj_rest_auth/utils.py | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/dj_rest_auth/tests/test_api.py b/dj_rest_auth/tests/test_api.py index 9554d05..f25f5fc 100644 --- a/dj_rest_auth/tests/test_api.py +++ b/dj_rest_auth/tests/test_api.py @@ -21,6 +21,16 @@ except ImportError: from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from jwt import decode as decode_jwt +class TESTTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + # Add custom claims + token['name'] = user.username + token['email'] = user.email + + return token + @override_settings(ROOT_URLCONF="tests.urls") class APIBasicTests(TestsMixin, TestCase): @@ -610,18 +620,6 @@ class APIBasicTests(TestsMixin, TestCase): self.assertEqual(resp.status_code, 500) - - class TESTTokenObtainPairSerializer(TokenObtainPairSerializer): - @classmethod - def get_token(cls, user): - token = super().get_token(user) - # Add custom claims - token['name'] = user.username - token['email'] = user.email - - return token - - @override_settings(REST_USE_JWT=True) @override_settings(JWT_AUTH_COOKIE=None) @override_settings(REST_FRAMEWORK=dict( @@ -630,7 +628,7 @@ class APIBasicTests(TestsMixin, TestCase): ] )) @override_settings(REST_SESSION_LOGIN=False) - @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = 'tests.test_api.TESTTokenObtainPairSerializer') def test_custom_jwt_claims(self): payload = { "username": self.USERNAME, @@ -655,7 +653,7 @@ class APIBasicTests(TestsMixin, TestCase): ] )) @override_settings(REST_SESSION_LOGIN=False) - @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = TESTTokenObtainPairSerializer) + @override_settings(JWT_TOKEN_CLAIMS_SERIALIZER = 'tests.test_api.TESTTokenObtainPairSerializer') def test_custom_jwt_claims_cookie_w_authentication(self): payload = { "username": self.USERNAME, diff --git a/dj_rest_auth/utils.py b/dj_rest_auth/utils.py index 5f229af..fa971de 100644 --- a/dj_rest_auth/utils.py +++ b/dj_rest_auth/utils.py @@ -21,7 +21,7 @@ try: from rest_framework_simplejwt.serializers import TokenObtainPairSerializer def jwt_encode(user): - TOPS = getattr(settings, 'JWT_TOKEN_CLAIMS_SERIALIZER', TokenObtainPairSerializer) + TOPS = import_callable(getattr(settings, 'JWT_TOKEN_CLAIMS_SERIALIZER', TokenObtainPairSerializer)) refresh = TOPS.get_token(user) return refresh.access_token, refresh From ed99925d724f53e9cc515335def0810be531fe11 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 20 Jun 2020 13:35:16 -0500 Subject: [PATCH 5/5] Adds docs --- docs/configuration.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index 3b363a8..f421d76 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -12,6 +12,8 @@ Configuration - JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``dj_rest_auth.views.LoginView``, default value ``dj_rest_auth.serializers.JWTSerializer`` + - JWT_TOKEN_CLAIMS_SERIALIZER - A custom JWT Claim serializer. Default is `rest_framework_simplejwt.serializers.TokenObtainPairSerializer` + - USER_DETAILS_SERIALIZER - serializer class in ``dj_rest_auth.views.UserDetailsView``, default value ``dj_rest_auth.serializers.UserDetailsSerializer`` - PASSWORD_RESET_SERIALIZER - serializer class in ``dj_rest_auth.views.PasswordResetView``, default value ``dj_rest_auth.serializers.PasswordResetSerializer``