Skip to content

Commit 30efd79

Browse files
Expect the remote exp to be defined in time zone UTC conform rfc (Fix… (#1292)
* Expect the remote exp to be defined in time zone UTC conform rfc (Fixes #1291) * deal with zoneinfo for python < 3.9 --------- Co-authored-by: Alan Crosswell <[email protected]>
1 parent 6ae8197 commit 30efd79

File tree

8 files changed

+126
-14
lines changed

8 files changed

+126
-14
lines changed

AUTHORS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,7 @@ Tom Evans
107107
Vinay Karanam
108108
Víðir Valberg Guðmundsson
109109
Will Beaufoy
110+
pySilver
111+
Łukasz Skarżyński
112+
Wouter Klein Heerenbrink
110113
Yuri Savin

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
## [unreleased]
1818

19+
### Fixed
20+
* #1292 Interpret `EXP` in AccessToken always as UTC instead of own key
21+
* #1292 Introduce setting `AUTHENTICATION_SERVER_EXP_TIME_ZONE` to enable different time zone in case remote
22+
authentication server doe snot provide EXP in UTC
23+
1924
### WARNING
2025
* If you are going to revert migration 0006 make note that previously hashed client_secret cannot be reverted
2126

docs/settings.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ The number of seconds an authorization token received from the introspection end
266266
If the expire time of the received token is less than ``RESOURCE_SERVER_TOKEN_CACHING_SECONDS`` the expire time
267267
will be used.
268268

269+
AUTHENTICATION_SERVER_EXP_TIME_ZONE
270+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
271+
The exp (expiration date) of Access Tokens must be defined in UTC (Unix Timestamp). Although its wrong, sometimes
272+
a remote Authentication Server does not use UTC (eg. no timezone support and configured in local time other than UTC).
273+
Prior to fix #1292 this could be fixed by changing your own time zone. With the introduction of this fix, this workaround
274+
would not be possible anymore. This setting re-enables this workaround.
269275

270276
PKCE_REQUIRED
271277
~~~~~~~~~~~~~

oauth2_provider/oauth2_validators.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
)
3939
from .scopes import get_scopes_backend
4040
from .settings import oauth2_settings
41+
from .utils import get_timezone
4142

4243

4344
log = logging.getLogger("oauth2_provider")
@@ -400,7 +401,11 @@ def _get_token_from_authentication_server(
400401
expires = max_caching_time
401402

402403
scope = content.get("scope", "")
403-
expires = make_aware(expires) if settings.USE_TZ else expires
404+
405+
if settings.USE_TZ:
406+
expires = make_aware(
407+
expires, timezone=get_timezone(oauth2_settings.AUTHENTICATION_SERVER_EXP_TIME_ZONE)
408+
)
404409

405410
access_token, _created = AccessToken.objects.update_or_create(
406411
token=token,

oauth2_provider/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@
102102
"RESOURCE_SERVER_AUTH_TOKEN": None,
103103
"RESOURCE_SERVER_INTROSPECTION_CREDENTIALS": None,
104104
"RESOURCE_SERVER_TOKEN_CACHING_SECONDS": 36000,
105+
# Authentication Server Exp Timezone: the time zone use dby Auth Server for generate EXP
106+
"AUTHENTICATION_SERVER_EXP_TIME_ZONE": "UTC",
105107
# Whether or not PKCE is required
106108
"PKCE_REQUIRED": True,
107109
# Whether to re-create OAuthlibCore on every request.

oauth2_provider/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22

3+
from django.conf import settings
34
from jwcrypto import jwk
45

56

@@ -10,3 +11,24 @@ def jwk_from_pem(pem_string):
1011
Converting from PEM is expensive for large keys such as those using RSA.
1112
"""
1213
return jwk.JWK.from_pem(pem_string.encode("utf-8"))
14+
15+
16+
# @functools.lru_cache
17+
def get_timezone(time_zone):
18+
"""
19+
Return the default time zone as a tzinfo instance.
20+
21+
This is the time zone defined by settings.TIME_ZONE.
22+
"""
23+
try:
24+
import zoneinfo
25+
except ImportError:
26+
import pytz
27+
28+
return pytz.timezone(time_zone)
29+
else:
30+
if getattr(settings, "USE_DEPRECATED_PYTZ", False):
31+
import pytz
32+
33+
return pytz.timezone(time_zone)
34+
return zoneinfo.ZoneInfo(time_zone)

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ install_requires =
4040
requests >= 2.13.0
4141
oauthlib >= 3.1.0
4242
jwcrypto >= 0.8.0
43+
pytz >= 2024.1
4344

4445
[options.packages.find]
4546
exclude =

tests/test_introspection_auth.py

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
AccessToken = get_access_token_model()
3030
UserModel = get_user_model()
3131

32-
exp = datetime.datetime.now() + datetime.timedelta(days=1)
32+
default_exp = datetime.datetime.now() + datetime.timedelta(days=1)
3333

3434

3535
class ScopeResourceView(ScopedProtectedResourceView):
@@ -42,27 +42,28 @@ def post(self, request, *args, **kwargs):
4242
return HttpResponse("This is a protected resource", 200)
4343

4444

45+
class MockResponse:
46+
def __init__(self, json_data, status_code):
47+
self.json_data = json_data
48+
self.status_code = status_code
49+
50+
def json(self):
51+
return self.json_data
52+
53+
4554
def mocked_requests_post(url, data, *args, **kwargs):
4655
"""
4756
Mock the response from the authentication server
4857
"""
4958

50-
class MockResponse:
51-
def __init__(self, json_data, status_code):
52-
self.json_data = json_data
53-
self.status_code = status_code
54-
55-
def json(self):
56-
return self.json_data
57-
5859
if "token" in data and data["token"] and data["token"] != "12345678900":
5960
return MockResponse(
6061
{
6162
"active": True,
6263
"scope": "read write dolphin",
6364
"client_id": "client_id_{}".format(data["token"]),
6465
"username": "{}_user".format(data["token"]),
65-
"exp": int(calendar.timegm(exp.timetuple())),
66+
"exp": int(calendar.timegm(default_exp.timetuple())),
6667
},
6768
200,
6869
)
@@ -75,6 +76,21 @@ def json(self):
7576
)
7677

7778

79+
def mocked_introspect_request_short_living_token(url, data, *args, **kwargs):
80+
exp = datetime.datetime.now() + datetime.timedelta(minutes=30)
81+
82+
return MockResponse(
83+
{
84+
"active": True,
85+
"scope": "read write dolphin",
86+
"client_id": "client_id_{}".format(data["token"]),
87+
"username": "{}_user".format(data["token"]),
88+
"exp": int(calendar.timegm(exp.timetuple())),
89+
},
90+
200,
91+
)
92+
93+
7894
urlpatterns = [
7995
path("oauth2/", include("oauth2_provider.urls")),
8096
path("oauth2-test-resource/", ScopeResourceView.as_view()),
@@ -152,24 +168,76 @@ def test_get_token_from_authentication_server_existing_token(self, mock_get):
152168
self.assertEqual(token.user.username, "foo_user")
153169
self.assertEqual(token.scope, "read write dolphin")
154170

155-
@mock.patch("requests.post", side_effect=mocked_requests_post)
156-
def test_get_token_from_authentication_server_expires_timezone(self, mock_get):
171+
@mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token)
172+
def test_get_token_from_authentication_server_expires_no_timezone(self, mock_get):
157173
"""
158174
Test method _get_token_from_authentication_server for projects with USE_TZ False
159175
"""
160176
settings_use_tz_backup = settings.USE_TZ
161177
settings.USE_TZ = False
162178
try:
163-
self.validator._get_token_from_authentication_server(
179+
access_token = self.validator._get_token_from_authentication_server(
180+
"foo",
181+
oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL,
182+
oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN,
183+
oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS,
184+
)
185+
186+
self.assertFalse(access_token.is_expired())
187+
except ValueError as exception:
188+
self.fail(str(exception))
189+
finally:
190+
settings.USE_TZ = settings_use_tz_backup
191+
192+
@mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token)
193+
def test_get_token_from_authentication_server_expires_utc_timezone(self, mock_get):
194+
"""
195+
Test method _get_token_from_authentication_server for projects with USE_TZ True and a UTC Timezone
196+
"""
197+
settings_use_tz_backup = settings.USE_TZ
198+
settings_time_zone_backup = settings.TIME_ZONE
199+
settings.USE_TZ = True
200+
settings.TIME_ZONE = "UTC"
201+
try:
202+
access_token = self.validator._get_token_from_authentication_server(
164203
"foo",
165204
oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL,
166205
oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN,
167206
oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS,
168207
)
208+
209+
self.assertFalse(access_token.is_expired())
210+
except ValueError as exception:
211+
self.fail(str(exception))
212+
finally:
213+
settings.USE_TZ = settings_use_tz_backup
214+
settings.TIME_ZONE = settings_time_zone_backup
215+
216+
@mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token)
217+
def test_get_token_from_authentication_server_expires_non_utc_timezone(self, mock_get):
218+
"""
219+
Test method _get_token_from_authentication_server for projects with USE_TZ True and a non UTC Timezone
220+
221+
This test is important to check if the UTC Exp. date gets converted correctly
222+
"""
223+
settings_use_tz_backup = settings.USE_TZ
224+
settings_time_zone_backup = settings.TIME_ZONE
225+
settings.USE_TZ = True
226+
settings.TIME_ZONE = "Europe/Amsterdam"
227+
try:
228+
access_token = self.validator._get_token_from_authentication_server(
229+
"foo",
230+
oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL,
231+
oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN,
232+
oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS,
233+
)
234+
235+
self.assertFalse(access_token.is_expired())
169236
except ValueError as exception:
170237
self.fail(str(exception))
171238
finally:
172239
settings.USE_TZ = settings_use_tz_backup
240+
settings.TIME_ZONE = settings_time_zone_backup
173241

174242
@mock.patch("requests.post", side_effect=mocked_requests_post)
175243
def test_validate_bearer_token(self, mock_get):

0 commit comments

Comments
 (0)