From 0fd855714e720bfab9a25a419214027d8ca2b982 Mon Sep 17 00:00:00 2001 From: antiline Date: Mon, 22 Apr 2019 18:24:19 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EB=AC=B4=EC=A4=91=EB=8B=A8=20=ED=82=A4=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EB=8B=A4?= =?UTF-8?q?=EC=88=98=EC=9D=98=20=ED=82=A4=EB=A1=9C=20=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 ++++++++++- ridi_django_oauth2/config.py | 19 +++++++++---------- ridi_django_oauth2/utils/token.py | 12 ++++++++---- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8067bb8..be57093 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,16 @@ AUTH_USER_MODEL = 'ridi_django_oauth2.RidiUser' # RIDI Setting RIDI_OAUTH2_JWT_SECRET = 'this-is-jwt-secret' +RIDI_OAUTH2_JWT_SECRETS = [ + { + 'secret': 'this-is-hs256-key', + 'alg': 'HS256', + }, + { + 'secret': 'this-is-rs256-public-key', + 'alg': 'RS256', + }, +] RIDI_OAUTH2_CLIENT_ID = 'this-is-client-id' RIDI_OAUTH2_CLIENT_SECRET = 'this-is-client-secret' @@ -41,7 +51,6 @@ REST_FRAMEWORK = { 'ridi_django_oauth2.rest_framework.authentication.OAuth2Authentication', ) } - ``` diff --git a/ridi_django_oauth2/config.py b/ridi_django_oauth2/config.py index 103c8c4..60a80a6 100644 --- a/ridi_django_oauth2/config.py +++ b/ridi_django_oauth2/config.py @@ -1,11 +1,12 @@ +from typing import List + from django.conf import settings from ridi_oauth2.introspector.dtos import JwtInfo class _Settings: - JWT_SECRET_NAME = 'RIDI_OAUTH2_JWT_SECRET' - JWT_ALGORITHM_NAME = 'RIDI_OAUTH2_JWT_ALGORITHM' + JWT_SECRETS = 'RIDI_OAUTH2_JWT_SECRETS' COOKIE_DOMAIN = 'RIDI_OAUTH2_COOKIE_DOMAIN' ACCESS_TOKEN_COOKIE_KEY = 'RIDI_OAUTH2_ACCESS_TOKEN_COOKIE_KEY' @@ -13,18 +14,16 @@ class _Settings: class _Default: - JWT_ALGORITHM = 'HS256' - COOKIE_DOMAIN = 'ridibooks.com' ACCESS_TOKEN_COOKIE_KEY = "ridi-at" REFRESH_TOKEN_COOKIE_KEY = "ridi-rt" # JwtInfo -_RIDI_OAUTH2_JWT_SECRET = getattr(settings, _Settings.JWT_SECRET_NAME) -_RIDI_OAUTH2_JWT_ALGORITHM = getattr(settings, _Settings.JWT_ALGORITHM_NAME, _Default.JWT_ALGORITHM) - -_JWT_INFO = JwtInfo(secret=_RIDI_OAUTH2_JWT_SECRET, algorithm=_RIDI_OAUTH2_JWT_ALGORITHM) +_RIDI_OAUTH2_JWT_SECRETS = getattr(settings, _Settings.JWT_SECRETS) +_JWT_INFOS = [ + JwtInfo(_RIDI_OAUTH2_JWT_SECRET['secret'], _RIDI_OAUTH2_JWT_SECRET['alg']) for _RIDI_OAUTH2_JWT_SECRET in _RIDI_OAUTH2_JWT_SECRETS +] # Cookie _RIDI_COOKIE_DOMAIN = getattr(settings, _Settings.COOKIE_DOMAIN, _Default.COOKIE_DOMAIN) @@ -34,8 +33,8 @@ class _Default: class RidiOAuth2Config: @staticmethod - def get_jwt_info() -> JwtInfo: - return _JWT_INFO + def get_jwt_infos() -> List[JwtInfo]: + return _JWT_INFOS @staticmethod def get_cookie_domain() -> str: diff --git a/ridi_django_oauth2/utils/token.py b/ridi_django_oauth2/utils/token.py index d280f42..ff8f818 100644 --- a/ridi_django_oauth2/utils/token.py +++ b/ridi_django_oauth2/utils/token.py @@ -17,10 +17,14 @@ def get_token_from_cookie(request: HttpRequest) -> TokenData: def get_token_info(token: str) -> typing.Optional[AccessTokenInfo]: - try: - token_info = JwtIntrospectHelper.introspect(jwt_info=RidiOAuth2Config.get_jwt_info(), access_token=token) - except (KeyError, ExpireTokenException, InvalidJwtSignatureException): - token_info = None + jwt_infos = RidiOAuth2Config.get_jwt_infos() + token_info = None + for jwt_info in jwt_infos: + try: + token_info = JwtIntrospectHelper.introspect(jwt_info, token) + + except (KeyError, ExpireTokenException, InvalidJwtSignatureException): + pass return token_info From 9f1b15aa8a547291a2d37e124ca3a31cdb246624 Mon Sep 17 00:00:00 2001 From: antiline Date: Tue, 23 Apr 2019 02:18:14 +0900 Subject: [PATCH 2/7] =?UTF-8?q?'RIDI=5FOAUTH2=5FJWT=5FSECRETS'=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - runcommand.py | 7 ++++++- runtests.py | 7 ++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be57093..2ce0603 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ AUTH_USER_MODEL = 'ridi_django_oauth2.RidiUser' # RIDI Setting -RIDI_OAUTH2_JWT_SECRET = 'this-is-jwt-secret' RIDI_OAUTH2_JWT_SECRETS = [ { 'secret': 'this-is-hs256-key', diff --git a/runcommand.py b/runcommand.py index 8377947..8410bc3 100644 --- a/runcommand.py +++ b/runcommand.py @@ -27,7 +27,12 @@ 'MIDDLEWARE_CLASSES': ( 'ridi_django_oauth2.middlewares.AuthenticationMiddleware', ), - 'RIDI_OAUTH2_JWT_SECRET': 'dummy_jwt_secret', + 'RIDI_OAUTH2_JWT_SECRETS': [ + { + 'secret': 'dummy_jwt_secret', + 'alg': 'HS256' + }, + ], 'RIDI_ridi_oauth2_ID': 'dummy_client_id', 'RIDI_ridi_oauth2_SECRET': 'dummy_client_secret', diff --git a/runtests.py b/runtests.py index 45a2292..915538b 100644 --- a/runtests.py +++ b/runtests.py @@ -27,7 +27,12 @@ 'MIDDLEWARE_CLASSES': ( 'ridi_django_oauth2.middlewares.AuthenticationMiddleware', ), - 'RIDI_OAUTH2_JWT_SECRET': 'dummy_jwt_secret', + 'RIDI_OAUTH2_JWT_SECRETS': [ + { + 'secret': 'dummy_jwt_secret', + 'alg': 'HS256' + }, + ], 'RIDI_ridi_oauth2_ID': 'dummy_client_id', 'RIDI_ridi_oauth2_SECRET': 'dummy_client_secret', From 704bcf6e3706cfc7a35d77572d98da6c9df30bcb Mon Sep 17 00:00:00 2001 From: antiline Date: Tue, 23 Apr 2019 03:29:34 +0900 Subject: [PATCH 3/7] =?UTF-8?q?jwt=20=ED=82=A4=EB=A5=BC=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=AC=20=EB=95=8C=20KID=20=EB=A1=9C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EA=B3=A0=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ ridi_django_oauth2/config.py | 11 ++++++----- ridi_django_oauth2/utils/token.py | 10 ++++------ ridi_oauth2/introspector/helpers.py | 17 ++++++++++++++++- runcommand.py | 1 + runtests.py | 1 + 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2ce0603..29c1d16 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,12 @@ AUTH_USER_MODEL = 'ridi_django_oauth2.RidiUser' # RIDI Setting RIDI_OAUTH2_JWT_SECRETS = [ { + 'kid': '0', 'secret': 'this-is-hs256-key', 'alg': 'HS256', }, { + 'kid': '1', 'secret': 'this-is-rs256-public-key', 'alg': 'RS256', }, diff --git a/ridi_django_oauth2/config.py b/ridi_django_oauth2/config.py index 60a80a6..10b0749 100644 --- a/ridi_django_oauth2/config.py +++ b/ridi_django_oauth2/config.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Dict from django.conf import settings @@ -21,9 +21,10 @@ class _Default: # JwtInfo _RIDI_OAUTH2_JWT_SECRETS = getattr(settings, _Settings.JWT_SECRETS) -_JWT_INFOS = [ - JwtInfo(_RIDI_OAUTH2_JWT_SECRET['secret'], _RIDI_OAUTH2_JWT_SECRET['alg']) for _RIDI_OAUTH2_JWT_SECRET in _RIDI_OAUTH2_JWT_SECRETS -] +_JWT_INFOS = { + (_RIDI_OAUTH2_JWT_SECRET['kid'], JwtInfo(_RIDI_OAUTH2_JWT_SECRET['secret'], _RIDI_OAUTH2_JWT_SECRET['alg'])) + for _RIDI_OAUTH2_JWT_SECRET in _RIDI_OAUTH2_JWT_SECRETS +} # Cookie _RIDI_COOKIE_DOMAIN = getattr(settings, _Settings.COOKIE_DOMAIN, _Default.COOKIE_DOMAIN) @@ -33,7 +34,7 @@ class _Default: class RidiOAuth2Config: @staticmethod - def get_jwt_infos() -> List[JwtInfo]: + def get_jwt_infos() -> Dict[str, JwtInfo]: return _JWT_INFOS @staticmethod diff --git a/ridi_django_oauth2/utils/token.py b/ridi_django_oauth2/utils/token.py index ff8f818..7a668fe 100644 --- a/ridi_django_oauth2/utils/token.py +++ b/ridi_django_oauth2/utils/token.py @@ -18,13 +18,11 @@ def get_token_from_cookie(request: HttpRequest) -> TokenData: def get_token_info(token: str) -> typing.Optional[AccessTokenInfo]: jwt_infos = RidiOAuth2Config.get_jwt_infos() - token_info = None - for jwt_info in jwt_infos: - try: - token_info = JwtIntrospectHelper.introspect(jwt_info, token) + try: + token_info = JwtIntrospectHelper.introspect(jwt_infos, token) - except (KeyError, ExpireTokenException, InvalidJwtSignatureException): - pass + except (KeyError, ExpireTokenException, InvalidJwtSignatureException): + token_info = None return token_info diff --git a/ridi_oauth2/introspector/helpers.py b/ridi_oauth2/introspector/helpers.py index 09448d4..fe7f36d 100644 --- a/ridi_oauth2/introspector/helpers.py +++ b/ridi_oauth2/introspector/helpers.py @@ -1,3 +1,7 @@ +from typing import Dict + +import jwt + from ridi_oauth2.introspector.dtos import AccessTokenInfo, JwtInfo from ridi_oauth2.introspector.exceptions import InvalidJwtSignatureException from ridi_oauth2.introspector.jwt_introspector import JwtIntrospector @@ -5,10 +9,21 @@ class JwtIntrospectHelper: @staticmethod - def introspect(jwt_info: JwtInfo, access_token: str) -> AccessTokenInfo: + def introspect(jwt_infos: Dict[str, JwtInfo], access_token: str) -> AccessTokenInfo: + unverified_header = jwt.get_unverified_header(access_token) + kid = unverified_header.get('kid') + if not kid: + raise InvalidJwtSignatureException + + jwt_info = jwt_infos.get(kid) + if not jwt_info: + raise InvalidJwtSignatureException + introspector = JwtIntrospector(jwt_info=jwt_info, access_token=access_token) result = introspector.introspect() + try: return AccessTokenInfo.from_dict(result) + except KeyError: raise InvalidJwtSignatureException diff --git a/runcommand.py b/runcommand.py index 8410bc3..0fe73e1 100644 --- a/runcommand.py +++ b/runcommand.py @@ -29,6 +29,7 @@ ), 'RIDI_OAUTH2_JWT_SECRETS': [ { + 'kid': '0', 'secret': 'dummy_jwt_secret', 'alg': 'HS256' }, diff --git a/runtests.py b/runtests.py index 915538b..990448b 100644 --- a/runtests.py +++ b/runtests.py @@ -29,6 +29,7 @@ ), 'RIDI_OAUTH2_JWT_SECRETS': [ { + 'kid': '0', 'secret': 'dummy_jwt_secret', 'alg': 'HS256' }, From 0f4d83c17b32d20119b8cb0493dc4db2a7d3a429 Mon Sep 17 00:00:00 2001 From: antiline Date: Tue, 23 Apr 2019 03:31:40 +0900 Subject: [PATCH 4/7] Update version. to 0.0.12 --- CHANGES.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c358b2c..6a2c92c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ Changelog ========= +0.0.12 (Apr 23th 2019) +------------------ +- Support select jwt sign key by kid + 0.0.11 (Apr 17th 2018) ------------------ - Change dependency version diff --git a/setup.py b/setup.py index 6a7dfc9..701909b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -version = '0.0.11' +version = '0.0.12' with open('requirements/base.txt') as f: install_requires = [line for line in f if line and not line.startswith('-')] From e8e9ae8c924de26cdadcc0a39f6f2d91f5a94860 Mon Sep 17 00:00:00 2001 From: antiline Date: Tue, 23 Apr 2019 03:38:23 +0900 Subject: [PATCH 5/7] travis run on xenial --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 37d6d5f..9ae677c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - '3.6' +dist: xenial stages: - test From c98a2b37919192ac4744fc3b0e1973a77de1efd1 Mon Sep 17 00:00:00 2001 From: antiline Date: Tue, 23 Apr 2019 03:48:24 +0900 Subject: [PATCH 6/7] Fix test case --- .../test_authentication_middleware.py | 9 ++++-- .../test_decorator.py | 28 +++++++++++++++---- .../test_token_util.py | 5 +++- tests/tests_ridi_oauth2/test_introspector.py | 7 +++-- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/tests/tests_ridi_django_oauth2/test_authentication_middleware.py b/tests/tests_ridi_django_oauth2/test_authentication_middleware.py index 0d6b4a8..836c5a9 100644 --- a/tests/tests_ridi_django_oauth2/test_authentication_middleware.py +++ b/tests/tests_ridi_django_oauth2/test_authentication_middleware.py @@ -14,6 +14,9 @@ class AuthenticationMiddlewareTestCase(TestCase): def setUp(self): self.middleware = AuthenticationMiddleware() + self.headers = { + 'kid': '0', + } self.valid_token = jwt.encode(payload={ 'sub': 'testuser', @@ -21,13 +24,13 @@ def setUp(self): 'exp': int(time.time()) + 60 * 60, 'client_id': 'asfeih29snv8as213i', 'scope': 'all' - }, key='dummy_jwt_secret').decode() + }, key='dummy_jwt_secret', headers=self.headers).decode() self.loose_token = jwt.encode(payload={ 'sub': 'testuser', 'u_idx': 123123, 'exp': int(time.time()) + 60 * 60, - }, key='dummy_jwt_secret').decode() + }, key='dummy_jwt_secret', headers=self.headers).decode() self.expire_token = jwt.encode(payload={ 'sub': 'testuser', @@ -35,7 +38,7 @@ def setUp(self): 'exp': int(time.time()) - 60 * 60, 'client_id': 'asfeih29snv8as213i', 'scope': 'all' - }, key='dummy_jwt_secret').decode() + }, key='dummy_jwt_secret', headers=self.headers).decode() def test_login_and_not_expire(self): request = Mock() diff --git a/tests/tests_ridi_django_oauth2/test_decorator.py b/tests/tests_ridi_django_oauth2/test_decorator.py index 1d44156..b8bd57a 100644 --- a/tests/tests_ridi_django_oauth2/test_decorator.py +++ b/tests/tests_ridi_django_oauth2/test_decorator.py @@ -26,6 +26,10 @@ def setUp(self): 'scope': 'all' } + self.headers = { + 'kid': '0', + } + self.dummy_view = login_required()(MagicMock(return_value=HttpResponse(content='success'))) self.custom_dummy_view = login_required(response_handler=response_handler)(MagicMock(return_value=HttpResponse(content='success'))) @@ -51,7 +55,9 @@ def test_not_login_with_custom_response(self): def test_not_exists_token_info(self): request = Mock() request.COOKIES = { - RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode(payload=self.jwt_payload, key='dummy_jwt_secret').decode(), + RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode( + self.jwt_payload, 'dummy_jwt_secret', headers=self.headers + ).decode(), } self.middleware.process_request(request) @@ -66,7 +72,9 @@ def test_not_exists_token_info(self): def test_login(self): request = Mock() request.COOKIES = { - RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode(payload=self.jwt_payload, key='dummy_jwt_secret').decode(), + RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode( + self.jwt_payload, 'dummy_jwt_secret', headers=self.headers + ).decode(), } self.middleware.process_request(request) @@ -95,6 +103,10 @@ def setUp(self): 'scope': 'user_info' } + self.headers = { + 'kid': '0', + } + self.dummy_view1 = scope_required(required_scopes=['user_info'])(MagicMock(return_value=HttpResponse(content='success1'))) self.dummy_view2 = scope_required(required_scopes=[('user_info', 'purchase')])( MagicMock(return_value=HttpResponse(content='success2')) @@ -106,7 +118,9 @@ def setUp(self): def test_all_scope(self): request = Mock() request.COOKIES = { - RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode(payload=self.jwt_payload, key='dummy_jwt_secret').decode(), + RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode( + self.jwt_payload, 'dummy_jwt_secret', headers=self.headers + ).decode(), } self.middleware.process_request(request) @@ -126,7 +140,9 @@ def test_all_scope(self): def test_restriction_scope(self): request = Mock() request.COOKIES = { - RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode(payload=self.jwt_loose_payload, key='dummy_jwt_secret').decode(), + RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode( + self.jwt_loose_payload, 'dummy_jwt_secret', headers=self.headers + ).decode(), } self.middleware.process_request(request) @@ -145,7 +161,9 @@ def test_restriction_scope(self): def test_restriction_scope_with_custom_response(self): request = Mock() request.COOKIES = { - RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode(payload=self.jwt_loose_payload, key='dummy_jwt_secret').decode(), + RidiOAuth2Config.get_access_token_cookie_key(): jwt.encode( + self.jwt_loose_payload, 'dummy_jwt_secret', headers=self.headers + ).decode(), } self.middleware.process_request(request) diff --git a/tests/tests_ridi_django_oauth2/test_token_util.py b/tests/tests_ridi_django_oauth2/test_token_util.py index 66b3d1f..b06a407 100644 --- a/tests/tests_ridi_django_oauth2/test_token_util.py +++ b/tests/tests_ridi_django_oauth2/test_token_util.py @@ -29,7 +29,10 @@ def test_token_info(self): 'client_id': 'asfeih29snv8as213i', 'scope': 'all' } - valid_token = jwt.encode(payload=payload, key='dummy_jwt_secret').decode() + headers = { + 'kid': '0', + } + valid_token = jwt.encode(payload=payload, key='dummy_jwt_secret', headers=headers).decode() token_info = get_token_info(token=valid_token) diff --git a/tests/tests_ridi_oauth2/test_introspector.py b/tests/tests_ridi_oauth2/test_introspector.py index 86dbd2b..24a3575 100644 --- a/tests/tests_ridi_oauth2/test_introspector.py +++ b/tests/tests_ridi_oauth2/test_introspector.py @@ -14,18 +14,21 @@ class JwtIntrospectorTestCase(unittest.TestCase): def setUp(self): self.secret = generate_random_str(chars=string.ascii_letters + string.digits + string.punctuation) self.alg = 'HS256' + self.headers = { + 'kid': '0', + } self.claim = { "sub": "testuser", "exp": int(time.time()) + 60 * 60, } - self.token = jwt.encode(self.claim, self.secret, algorithm=self.alg) + self.token = jwt.encode(self.claim, self.secret, self.alg, self.headers) self.invalid_claim = { "sub": "testuser", "exp": int(time.time()) - 60 * 60, } - self.invalid_token = jwt.encode(self.invalid_claim, self.secret, algorithm=self.alg) + self.invalid_token = jwt.encode(self.invalid_claim, self.secret, self.alg, self.headers) def test_introspect(self): jwt_info = JwtInfo(secret=self.secret, algorithm=self.alg) From ade71837c5eaf0d1fe2061b9485ceb12bbd2974c Mon Sep 17 00:00:00 2001 From: antiline Date: Tue, 23 Apr 2019 03:54:42 +0900 Subject: [PATCH 7/7] Fix test case --- ridi_django_oauth2/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ridi_django_oauth2/config.py b/ridi_django_oauth2/config.py index 10b0749..64fdde3 100644 --- a/ridi_django_oauth2/config.py +++ b/ridi_django_oauth2/config.py @@ -21,10 +21,10 @@ class _Default: # JwtInfo _RIDI_OAUTH2_JWT_SECRETS = getattr(settings, _Settings.JWT_SECRETS) -_JWT_INFOS = { +_JWT_INFOS = dict([ (_RIDI_OAUTH2_JWT_SECRET['kid'], JwtInfo(_RIDI_OAUTH2_JWT_SECRET['secret'], _RIDI_OAUTH2_JWT_SECRET['alg'])) for _RIDI_OAUTH2_JWT_SECRET in _RIDI_OAUTH2_JWT_SECRETS -} +]) # Cookie _RIDI_COOKIE_DOMAIN = getattr(settings, _Settings.COOKIE_DOMAIN, _Default.COOKIE_DOMAIN)