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 .scopesimportget_scopes_backend
4040
from .settingsimportoauth2_settings
41+
from .utilsimportget_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) ifsettings.USE_TZelseexpires
404+
405+
ifsettings.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
importfunctools
22

3+
fromdjango.confimportsettings
34
fromjwcryptoimportjwk
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
returnjwk.JWK.from_pem(pem_string.encode("utf-8"))
14+
15+
16+
# @functools.lru_cache
17+
defget_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+
importzoneinfo
25+
exceptImportError:
26+
importpytz
27+
28+
returnpytz.timezone(time_zone)
29+
else:
30+
ifgetattr(settings, "USE_DEPRECATED_PYTZ", False):
31+
importpytz
32+
33+
returnpytz.timezone(time_zone)
34+
returnzoneinfo.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
classScopeResourceView(ScopedProtectedResourceView):
@@ -42,27 +42,28 @@ def post(self, request, *args, **kwargs):
4242
returnHttpResponse("This is a protected resource", 200)
4343

4444

45+
classMockResponse:
46+
def__init__(self, json_data, status_code):
47+
self.json_data=json_data
48+
self.status_code=status_code
49+
50+
defjson(self):
51+
returnself.json_data
52+
53+
4554
defmocked_requests_post(url, data, *args, **kwargs):
4655
"""
4756
Mock the response from the authentication server
4857
"""
4958

50-
classMockResponse:
51-
def__init__(self, json_data, status_code):
52-
self.json_data=json_data
53-
self.status_code=status_code
54-
55-
defjson(self):
56-
returnself.json_data
57-
5859
if"token"indataanddata["token"] anddata["token"] !="12345678900":
5960
returnMockResponse(
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+
defmocked_introspect_request_short_living_token(url, data, *args, **kwargs):
80+
exp=datetime.datetime.now() +datetime.timedelta(minutes=30)
81+
82+
returnMockResponse(
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-
deftest_get_token_from_authentication_server_expires_timezone(self, mock_get):
171+
@mock.patch("requests.post", side_effect=mocked_introspect_request_short_living_token)
172+
deftest_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+
exceptValueErrorasexception:
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+
deftest_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+
exceptValueErrorasexception:
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+
deftest_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
exceptValueErrorasexception:
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
deftest_validate_bearer_token(self, mock_get):

0 commit comments

Comments
(0)