Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expect the remote exp to be defined in time zone UTC conform rfc (Fix… #1292

Merged
merged 4 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,7 @@ Tom Evans
Vinay Karanam
Víðir Valberg Guðmundsson
Will Beaufoy
pySilver
Łukasz Skarżyński
Wouter Klein Heerenbrink
Yuri Savin
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### WARNING
* If you are going to revert migration 0006 make note that previously hashed client_secret cannot be reverted

Expand Down
6 changes: 6 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,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
~~~~~~~~~~~~~
Expand Down
7 changes: 6 additions & 1 deletion oauth2_provider/oauth2_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
)
from .scopes import get_scopes_backend
from .settings import oauth2_settings
from .utils import get_timezone


log = logging.getLogger("oauth2_provider")
Expand Down Expand Up @@ -400,7 +401,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,
Expand Down
2 changes: 2 additions & 0 deletions oauth2_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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.
Expand Down
22 changes: 22 additions & 0 deletions oauth2_provider/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools

from django.conf import settings
from jwcrypto import jwk


Expand All @@ -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
n2ygk marked this conversation as resolved.
Show resolved Hide resolved
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)
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ install_requires =
requests >= 2.13.0
oauthlib >= 3.1.0
jwcrypto >= 0.8.0
pytz >= 2024.1
n2ygk marked this conversation as resolved.
Show resolved Hide resolved

[options.packages.find]
exclude =
Expand Down
94 changes: 81 additions & 13 deletions tests/test_introspection_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -42,27 +42,28 @@ 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(
{
"active": True,
"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,
)
Expand All @@ -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()),
Expand Down Expand Up @@ -152,24 +168,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):
Expand Down
Loading