Skip to content

Commit 7d1b8b1

Browse files
authored
Merge pull request #122 from oslokommune/unjwt
Don't rely on the `exp` fields of unsigned JWTs
2 parents 69f51f6 + 7a8139f commit 7d1b8b1

File tree

8 files changed

+49
-61
lines changed

8 files changed

+49
-61
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Removed dependency on the vulnerable (and seemingly abandoned) python-jose
44
library.
5+
* PyJWT is no longer a dependency.
56

67
## 3.1.0 - 2024-01-10
78

okdata/sdk/auth/auth.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
import json
22
import logging
3+
from datetime import datetime, timedelta, timezone
34

45
from okdata.sdk.auth.credentials.client_credentials import ClientCredentialsProvider
56
from okdata.sdk.auth.credentials.common import (
67
TokenProviderNotInitialized,
78
TokenRefreshError,
89
)
910
from okdata.sdk.auth.credentials.password_grant import TokenServiceProvider
10-
from okdata.sdk.auth.util import is_token_expired
1111
from okdata.sdk.exceptions import ApiAuthenticateError
1212
from okdata.sdk.file_cache import FileCache
1313

1414
log = logging.getLogger()
1515

1616

17-
class Authenticate(object):
17+
def _is_expired(timestamp):
18+
"""Return true if `timestamp` has expired (or is just about to expire)."""
19+
return timestamp and (timestamp - datetime.now(timezone.utc)).total_seconds() < 10
20+
21+
22+
class Authenticate:
23+
_access_token = None
24+
_refresh_token = None
25+
_expires_at = None
26+
_refresh_expires_at = None
27+
1828
def __init__(self, config, token_provider=None, file_cache=None):
1929
self.token_provider = token_provider
2030
if not self.token_provider:
@@ -30,9 +40,6 @@ def __init__(self, config, token_provider=None, file_cache=None):
3040
if not self.file_cache:
3141
self.file_cache = FileCache(config)
3242

33-
self._access_token = None
34-
self._refresh_token = None
35-
3643
def _resolve_token_provider(self, config):
3744
# Add more TokenProviders to accept different login methods
3845
strategies = [ClientCredentialsProvider, TokenServiceProvider]
@@ -54,7 +61,7 @@ def access_token(self):
5461
if not self._access_token:
5562
self.login()
5663
# If expired, refresh
57-
if is_token_expired(self._access_token):
64+
if _is_expired(self._expires_at):
5865
self.refresh_access_token()
5966
return self._access_token
6067

@@ -66,8 +73,12 @@ def login(self, force=False):
6673
if cached:
6774
self._access_token = cached["access_token"]
6875
self._refresh_token = cached.get("refresh_token")
76+
if expires_at := cached.get("expires_at"):
77+
self._expires_at = datetime.fromisoformat(expires_at)
78+
if refresh_expires_at := cached.get("refresh_expires_at"):
79+
self._refresh_expires_at = datetime.fromisoformat(refresh_expires_at)
6980

70-
if self._access_token and not is_token_expired(self._access_token):
81+
if self._access_token and not _is_expired(self._expires_at):
7182
log.info("Token not expired, skipping")
7283
return
7384
self.refresh_access_token()
@@ -78,7 +89,7 @@ def refresh_access_token(self):
7889

7990
tokens = None
8091

81-
if self._refresh_token and not is_token_expired(self._refresh_token):
92+
if self._refresh_token and not _is_expired(self._refresh_expires_at):
8293
try:
8394
tokens = self.token_provider.refresh_token(self._refresh_token)
8495
except TokenRefreshError as e:
@@ -89,6 +100,13 @@ def refresh_access_token(self):
89100
if "access_token" not in tokens:
90101
raise ApiAuthenticateError
91102
self._refresh_token = tokens.get("refresh_token")
103+
self._expires_at = datetime.now(timezone.utc) + timedelta(
104+
seconds=tokens.get("expires_in")
105+
)
106+
if refresh_expires_in := tokens.get("refresh_expires_in"):
107+
self._refresh_expires_at = datetime.now(timezone.utc) + timedelta(
108+
seconds=refresh_expires_in
109+
)
92110

93111
self._access_token = tokens["access_token"]
94112
self.file_cache.write_credentials(credentials=self)
@@ -99,6 +117,12 @@ def __repr__(self):
99117
"provider": self.token_provider.__class__.__name__,
100118
"access_token": self._access_token,
101119
"refresh_token": self._refresh_token,
120+
"expires_at": self._expires_at.isoformat() if self._expires_at else "",
121+
"refresh_expires_at": (
122+
self._refresh_expires_at.isoformat()
123+
if self._refresh_expires_at
124+
else ""
125+
),
102126
}
103127
)
104128

okdata/sdk/auth/util.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

requirements.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ packaging==24.0
2626
# via deprecation
2727
pycparser==2.22
2828
# via cffi
29-
pyjwt==2.4.0
30-
# via okdata-sdk (setup.py)
3129
pyrsistent==0.18.1
3230
# via jsonschema
3331
python-keycloak==3.11.1

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
namespace_packages=["okdata"],
2020
install_requires=[
2121
"jsonschema",
22-
"PyJWT>=2.0.0",
2322
# Versions prior to 3.9.1 depends on the vulnerable (and seemingly
2423
# abandoned) python-jose library.
2524
"python-keycloak>=3.9.1,<4",

tests/auth/auth_test.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
from freezegun import freeze_time
1212

1313
from tests.auth.client_credentials_test_utils import (
14-
from_cache_not_expired_token,
14+
expired_time,
1515
from_cache_expired_token,
16+
from_cache_not_expired_token,
17+
not_expired_time,
1618
utc_now,
1719
)
1820
from tests.test_utils import (
@@ -71,6 +73,8 @@ def test_authenticate_cached_credentials(self, mock_home_dir):
7173
"provider": "ClientCredentialsProvider",
7274
"access_token": from_cache_not_expired_token,
7375
"refresh_token": from_cache_not_expired_token,
76+
"expires_at": not_expired_time.isoformat(),
77+
"refresh_expires_at": not_expired_time.isoformat(),
7478
}
7579

7680
auth.file_cache.write_credentials(json.dumps(cached_credentials))
@@ -88,6 +92,8 @@ def test_authenticate_refresh_credentials(self, requests_mock, mock_home_dir):
8892
"provider": "ClientCredentialsProvider",
8993
"access_token": from_cache_not_expired_token,
9094
"refresh_token": from_cache_not_expired_token,
95+
"expires_at": not_expired_time.isoformat(),
96+
"refresh_expires_at": not_expired_time.isoformat(),
9197
}
9298

9399
auth.file_cache.write_credentials(json.dumps(cached_credentials))
@@ -110,6 +116,8 @@ def test_authenticate_expired_tokens(self, requests_mock, mock_home_dir):
110116
"provider": "TokenServiceProvider",
111117
"access_token": from_cache_expired_token,
112118
"refresh_token": from_cache_expired_token,
119+
"expires_at": expired_time.isoformat(),
120+
"refresh_expires_at": expired_time.isoformat(),
113121
}
114122

115123
auth.file_cache.write_credentials(json.dumps(cached_credentials))
@@ -134,6 +142,8 @@ def test_authenticate_expired_access_token(self, requests_mock, mock_home_dir):
134142
"provider": "TokenServiceProvider",
135143
"access_token": from_cache_expired_token,
136144
"refresh_token": from_cache_not_expired_token,
145+
"expires_at": expired_time.isoformat(),
146+
"refresh_expires_at": not_expired_time.isoformat(),
137147
}
138148

139149
auth.file_cache.write_credentials(json.dumps(cached_credentials))
@@ -173,6 +183,8 @@ def test_refresh_inactive_session(self, requests_mock, mock_home_dir):
173183
"provider": "TokenServiceProvider",
174184
"access_token": from_cache_expired_token,
175185
"refresh_token": from_cache_not_expired_token,
186+
"expires_at": expired_time.isoformat(),
187+
"refresh_expires_at": not_expired_time.isoformat(),
176188
}
177189

178190
auth.file_cache.write_credentials(json.dumps(cached_credentials))
@@ -203,6 +215,7 @@ def test_refresh_no_refresh_token(self, requests_mock, mock_home_dir):
203215
cached_credentials = {
204216
"provider": "TokenServiceProvider",
205217
"access_token": from_cache_expired_token,
218+
"expires_at": expired_time.isoformat(),
206219
}
207220

208221
auth.file_cache.write_credentials(json.dumps(cached_credentials))

tests/auth/token_utils_test.py

Lines changed: 0 additions & 22 deletions
This file was deleted.

tox.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ envlist = py38,py39,py310,py311,py312,flake8,black,mypy
44
[testenv]
55
deps=
66
freezegun==0.3.12
7-
pytest==6.2.5
7+
PyJWT==2.8.0
88
pytest-mock==1.12.1
9+
pytest==6.2.5
910
python-dateutil==2.8.0
1011
requests-mock==1.6.0
1112
-rrequirements.txt

0 commit comments

Comments
 (0)