Skip to content

Commit 4ca2890

Browse files
committed
feat: better jwt expiration handling
1 parent 99fdc51 commit 4ca2890

4 files changed

Lines changed: 58 additions & 3 deletions

File tree

libs/foundry-dev-tools/src/foundry_dev_tools/config/context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def __init__(
7373
self.client.headers["User-Agent"] = requests.utils.default_user_agent(
7474
f"foundry-dev-tools/{__version__}/python-requests"
7575
)
76+
self.token_provider.set_expiration(self)
7677

7778
if self.config.rich_traceback:
7879
from rich.traceback import install

libs/foundry-dev-tools/src/foundry_dev_tools/config/token_provider.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@
33
from __future__ import annotations
44

55
import base64
6+
import json
67
import time
7-
from functools import cached_property
8+
import uuid
89
from typing import TYPE_CHECKING, ClassVar
910

1011
import palantir_oauth_client
1112
import requests
1213
from requests.structures import CaseInsensitiveDict
1314

1415
from foundry_dev_tools.config.config_types import Host
15-
from foundry_dev_tools.errors.config import TokenProviderConfigError
16+
from foundry_dev_tools.errors.config import InvalidOrExpiredJWTTokenError, TokenProviderConfigError
1617
from foundry_dev_tools.errors.handling import ErrorHandlingConfig, raise_foundry_api_error
1718
from foundry_dev_tools.errors.multipass import ClientAuthenticationFailedError
1819
from foundry_dev_tools.utils.config import entry_point_fdt_token_provider
1920

2021
if TYPE_CHECKING:
2122
from foundry_dev_tools.config.config_types import FoundryOAuthGrantType, Token
23+
from foundry_dev_tools.config.context import FoundryContext
2224

2325

2426
class TokenProvider:
@@ -56,10 +58,18 @@ def requests_auth_handler(self, r: requests.PreparedRequest) -> requests.Prepare
5658
def set_requests_session(self, session: requests.Session) -> None:
5759
"""No-op by default."""
5860

61+
def set_expiration(self, ctx: FoundryContext) -> None:
62+
"""No-op by default.
63+
64+
Used for jwt expiration retrieval.
65+
"""
66+
5967

6068
class JWTTokenProvider(TokenProvider):
6169
"""Provides Host and Token."""
6270

71+
__expiration: int | None = None
72+
6373
def __init__(self, host: Host | str, jwt: Token) -> None:
6474
"""Initialize the JWTTokenProvider.
6575
@@ -70,9 +80,39 @@ def __init__(self, host: Host | str, jwt: Token) -> None:
7080
super().__init__(host)
7181
self._jwt = jwt
7282

73-
@cached_property
83+
def _decode_token_id(self) -> str:
84+
split_jwt = self._jwt.split(".")
85+
if len(split_jwt) < 2:
86+
raise InvalidOrExpiredJWTTokenError
87+
try:
88+
claim_part = split_jwt[1] + "==" # add padding, which is omitted by jwt
89+
claim_json = base64.b64decode(claim_part)
90+
claims = json.loads(claim_json)
91+
jti = base64.b64decode(claims["jti"])
92+
return str(uuid.UUID(bytes=jti))
93+
except Exception as e:
94+
raise InvalidOrExpiredJWTTokenError from e
95+
96+
def set_expiration(self, ctx: FoundryContext) -> None:
97+
"""This method decodes the jwt token and retrieves its expiration."""
98+
if self.__expiration:
99+
return
100+
token_id = self._decode_token_id()
101+
try:
102+
tokens = ctx.multipass.get_tokens() # retrieve all tokens the user has generated
103+
for token in tokens:
104+
if token["tokenId"] == token_id: # search for this jwt token
105+
# save expiration time, which will be used in the token() property
106+
self.__expiration = time.time() + token["expires_in"] - 15 # subtract 15 seconds, to be safe
107+
break
108+
except: # noqa: E722
109+
raise InvalidOrExpiredJWTTokenError from None
110+
111+
@property
74112
def token(self) -> Token:
75113
"""Returns the token supplied when creating this Provider."""
114+
if self.__expiration and self.__expiration <= time.time():
115+
raise InvalidOrExpiredJWTTokenError
76116
return self._jwt
77117

78118

libs/foundry-dev-tools/src/foundry_dev_tools/errors/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ class MissingFoundryHostError(TokenProviderConfigError):
3737

3838
def __init__(self) -> None:
3939
super().__init__("A domain is missing in your credentials configuration.")
40+
41+
42+
class InvalidOrExpiredJWTTokenError(FoundryConfigError):
43+
"""Error if jwt is either expired or invalid."""
44+
45+
def __init__(self) -> None:
46+
super().__init__(
47+
"The JWT Token you provided in the config, is either expired or invalid.\n"
48+
"Please generate a new JWT Token and update your configuration."
49+
)

tests/unit/mocks.py

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

3+
import time
34
from typing import TYPE_CHECKING
45

56
import requests_mock
@@ -30,6 +31,9 @@ def __init__(self, jwt: Token):
3031
self._jwt = jwt
3132
self.host = TEST_HOST
3233

34+
def set_expiration(self, _):
35+
self.__expiration = int(time.time() + 3600)
36+
3337

3438
class MockOAuthTokenProvider(OAuthTokenProvider):
3539
"""The mock OAuth token provider, set the host to :py:attr:`TEST_HOST`"""

0 commit comments

Comments
 (0)