From b2e28285c7ee0e361f089456515232d56f7eea15 Mon Sep 17 00:00:00 2001 From: Wouter Klein Heerenbrink Date: Thu, 22 Jun 2023 18:06:17 +0200 Subject: [PATCH 1/2] Expect the remote exp to be defined in time zone UTC conform rfc (Fixes #1291) --- AUTHORS | 1 + CHANGELOG.md | 5 ++ docs/settings.rst | 6 ++ oauth2_provider/oauth2_validators.py | 7 ++- oauth2_provider/settings.py | 2 + oauth2_provider/utils.py | 22 +++++++ tests/test_introspection_auth.py | 94 ++++++++++++++++++++++++---- 7 files changed, 123 insertions(+), 14 deletions(-) diff --git a/AUTHORS b/AUTHORS index 16c2058b8..06629ddfb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -95,3 +95,4 @@ Víðir Valberg Guðmundsson Will Beaufoy pySilver Łukasz Skarżyński +Wouter Klein Heerenbrink diff --git a/CHANGELOG.md b/CHANGELOG.md index 292300ce2..3c13dd791 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Fixed +* #1292 Interpret `EXP` in AccessToken always as UTC instead of own key +* #1292 Introduce setting `AUTHENTICATION_SERVER_EXP_TIME_ZONE` to enable different time zone in case remote + authentication server doe snot provide EXP in UTC + ### Added * #1273 Add caching of loading of OIDC private key. * #1285 Add post_logout_redirect_uris field in application views. diff --git a/docs/settings.rst b/docs/settings.rst index f31aff533..188fecb74 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -255,6 +255,12 @@ The number of seconds an authorization token received from the introspection end If the expire time of the received token is less than ``RESOURCE_SERVER_TOKEN_CACHING_SECONDS`` the expire time will be used. +AUTHENTICATION_SERVER_EXP_TIME_ZONE +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The exp (expiration date) of Access Tokens must be defined in UTC (Unix Timestamp). Although its wrong, sometimes +a remote Authentication Server does not use UTC (eg. no timezone support and configured in local time other than UTC). +Prior to fix #1292 this could be fixed by changing your own time zone. With the introduction of this fix, this workaround +would not be possible anymore. This setting re-enables this workaround. PKCE_REQUIRED ~~~~~~~~~~~~~ diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index ecff21880..58d2cf014 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -37,6 +37,7 @@ ) from .scopes import get_scopes_backend from .settings import oauth2_settings +from .utils import get_timezone log = logging.getLogger("oauth2_provider") @@ -388,7 +389,11 @@ def _get_token_from_authentication_server( expires = max_caching_time scope = content.get("scope", "") - expires = make_aware(expires) if settings.USE_TZ else expires + + if settings.USE_TZ: + expires = make_aware( + expires, timezone=get_timezone(oauth2_settings.AUTHENTICATION_SERVER_EXP_TIME_ZONE) + ) access_token, _created = AccessToken.objects.update_or_create( token=token, diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index aa7de7351..03a8fb18c 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -101,6 +101,8 @@ "RESOURCE_SERVER_AUTH_TOKEN": None, "RESOURCE_SERVER_INTROSPECTION_CREDENTIALS": None, "RESOURCE_SERVER_TOKEN_CACHING_SECONDS": 36000, + # Authentication Server Exp Timezone: the time zone use dby Auth Server for generate EXP + "AUTHENTICATION_SERVER_EXP_TIME_ZONE": "UTC", # Whether or not PKCE is required "PKCE_REQUIRED": True, # Whether to re-create OAuthlibCore on every request. diff --git a/oauth2_provider/utils.py b/oauth2_provider/utils.py index de641f74f..36df66ff9 100644 --- a/oauth2_provider/utils.py +++ b/oauth2_provider/utils.py @@ -1,5 +1,6 @@ import functools +from django.conf import settings from jwcrypto import jwk @@ -10,3 +11,24 @@ def jwk_from_pem(pem_string): Converting from PEM is expensive for large keys such as those using RSA. """ return jwk.JWK.from_pem(pem_string.encode("utf-8")) + + +@functools.lru_cache +def get_timezone(time_zone): + """ + Return the default time zone as a tzinfo instance. + + This is the time zone defined by settings.TIME_ZONE. + """ + try: + import zoneinfo + except ImportError: + import pytz + + return pytz.timezone(time_zone) + else: + if getattr(settings, "USE_DEPRECATED_PYTZ", False): + import pytz + + return pytz.timezone(time_zone) + return zoneinfo.ZoneInfo(time_zone) diff --git a/tests/test_introspection_auth.py b/tests/test_introspection_auth.py index 8b2a6daf0..d69e65fc7 100644 --- a/tests/test_introspection_auth.py +++ b/tests/test_introspection_auth.py @@ -29,7 +29,7 @@ AccessToken = get_access_token_model() UserModel = get_user_model() -exp = datetime.datetime.now() + datetime.timedelta(days=1) +default_exp = datetime.datetime.now() + datetime.timedelta(days=1) class ScopeResourceView(ScopedProtectedResourceView): @@ -42,19 +42,20 @@ def post(self, request, *args, **kwargs): return HttpResponse("This is a protected resource", 200) +class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + def mocked_requests_post(url, data, *args, **kwargs): """ Mock the response from the authentication server """ - class MockResponse: - def __init__(self, json_data, status_code): - self.json_data = json_data - self.status_code = status_code - - def json(self): - return self.json_data - if "token" in data and data["token"] and data["token"] != "12345678900": return MockResponse( { @@ -62,7 +63,7 @@ def json(self): "scope": "read write dolphin", "client_id": "client_id_{}".format(data["token"]), "username": "{}_user".format(data["token"]), - "exp": int(calendar.timegm(exp.timetuple())), + "exp": int(calendar.timegm(default_exp.timetuple())), }, 200, ) @@ -75,6 +76,21 @@ def json(self): ) +def mocked_introspect_request_short_living_token(url, data, *args, **kwargs): + exp = datetime.datetime.now() + datetime.timedelta(minutes=30) + + return MockResponse( + { + "active": True, + "scope": "read write dolphin", + "client_id": "client_id_{}".format(data["token"]), + "username": "{}_user".format(data["token"]), + "exp": int(calendar.timegm(exp.timetuple())), + }, + 200, + ) + + urlpatterns = [ path("oauth2/", include("oauth2_provider.urls")), path("oauth2-test-resource/", ScopeResourceView.as_view()), @@ -156,24 +172,76 @@ def test_get_token_from_authentication_server_existing_token(self, mock_get): self.assertEqual(token.user.username, "foo_user") self.assertEqual(token.scope, "read write dolphin") - @mock.patch("requests.post", side_effect=mocked_requests_post) - def test_get_token_from_authentication_server_expires_timezone(self, mock_get): + @mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token) + def test_get_token_from_authentication_server_expires_no_timezone(self, mock_get): """ Test method _get_token_from_authentication_server for projects with USE_TZ False """ settings_use_tz_backup = settings.USE_TZ settings.USE_TZ = False try: - self.validator._get_token_from_authentication_server( + access_token = self.validator._get_token_from_authentication_server( + "foo", + oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL, + oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN, + oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS, + ) + + self.assertFalse(access_token.is_expired()) + except ValueError as exception: + self.fail(str(exception)) + finally: + settings.USE_TZ = settings_use_tz_backup + + @mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token) + def test_get_token_from_authentication_server_expires_utc_timezone(self, mock_get): + """ + Test method _get_token_from_authentication_server for projects with USE_TZ True and a UTC Timezone + """ + settings_use_tz_backup = settings.USE_TZ + settings_time_zone_backup = settings.TIME_ZONE + settings.USE_TZ = True + settings.TIME_ZONE = "UTC" + try: + access_token = self.validator._get_token_from_authentication_server( "foo", oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL, oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN, oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS, ) + + self.assertFalse(access_token.is_expired()) + except ValueError as exception: + self.fail(str(exception)) + finally: + settings.USE_TZ = settings_use_tz_backup + settings.TIME_ZONE = settings_time_zone_backup + + @mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token) + def test_get_token_from_authentication_server_expires_non_utc_timezone(self, mock_get): + """ + Test method _get_token_from_authentication_server for projects with USE_TZ True and a non UTC Timezone + + This test is important to check if the UTC Exp. date gets converted correctly + """ + settings_use_tz_backup = settings.USE_TZ + settings_time_zone_backup = settings.TIME_ZONE + settings.USE_TZ = True + settings.TIME_ZONE = "Europe/Amsterdam" + try: + access_token = self.validator._get_token_from_authentication_server( + "foo", + oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL, + oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN, + oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS, + ) + + self.assertFalse(access_token.is_expired()) except ValueError as exception: self.fail(str(exception)) finally: settings.USE_TZ = settings_use_tz_backup + settings.TIME_ZONE = settings_time_zone_backup @mock.patch("requests.post", side_effect=mocked_requests_post) def test_validate_bearer_token(self, mock_get): From 5596ad8d7514dfa084435b82ffd1f993ddb8f396 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 7 May 2024 12:44:01 -0400 Subject: [PATCH 2/2] deal with zoneinfo for python < 3.9 --- oauth2_provider/utils.py | 2 +- setup.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/utils.py b/oauth2_provider/utils.py index 36df66ff9..3f48723c5 100644 --- a/oauth2_provider/utils.py +++ b/oauth2_provider/utils.py @@ -13,7 +13,7 @@ def jwk_from_pem(pem_string): return jwk.JWK.from_pem(pem_string.encode("utf-8")) -@functools.lru_cache +# @functools.lru_cache def get_timezone(time_zone): """ Return the default time zone as a tzinfo instance. diff --git a/setup.cfg b/setup.cfg index 453126c28..d015d1238 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ install_requires = requests >= 2.13.0 oauthlib >= 3.1.0 jwcrypto >= 0.8.0 + pytz >= 2024.1 [options.packages.find] exclude =