11import datetime
2+ import hashlib
3+ from dataclasses import dataclass
24
35import jwt
46from ory_client .api .o_auth2_api import OAuth2Api as OryOAuth2Api
1618 KIWIX_ISSUER ,
1719 ORY_ACCESS_TOKEN ,
1820)
21+ from zimfarm_backend .common import getnow
1922
2023_ory_client_configuration = OryClientConfiguration (
2124 host = KIWIX_ISSUER , access_token = ORY_ACCESS_TOKEN
2225)
2326
2427
28+ @dataclass
29+ class _CachedToken :
30+ """Cached introspected token with expiration time."""
31+
32+ introspected_token : IntrospectedOAuth2Token
33+ expires_at : datetime .datetime
34+
35+
36+ # Cache to store introspected tokens with their expiration time
37+ # Key is SHA256 hash of the token, value is _CachedToken
38+ _introspection_token_cache : dict [str , _CachedToken ] = {}
39+
40+
41+ def _hash_token (token : str ) -> str :
42+ """Hash token for use as cache key to avoid storing raw tokens in memory."""
43+ return hashlib .sha256 (token .encode ()).hexdigest ()
44+
45+
46+ def _is_cache_entry_valid (cached : _CachedToken ) -> bool :
47+ """Check if a cached token entry is still valid (not expired)."""
48+ return getnow () < cached .expires_at
49+
50+
51+ def _get_cached_introspection_token (token : str ) -> IntrospectedOAuth2Token | None :
52+ """Get cached introspection result if available and not expired."""
53+ token_hash = _hash_token (token )
54+ if cached := _introspection_token_cache .get (token_hash ):
55+ if _is_cache_entry_valid (cached ):
56+ return cached .introspected_token
57+ # Remove expired entry
58+ del _introspection_token_cache [token_hash ]
59+ return None
60+
61+
62+ def _cache_introspection_token (token : str , introspected_token : IntrospectedOAuth2Token ):
63+ """Cache an introspected token with TTL based on its expiration time."""
64+ token_hash = _hash_token (token )
65+ # Use token's exp time if available, otherwise cache for a short duration
66+ if introspected_token .exp :
67+ # exp is a Unix timestamp (seconds since epoch)
68+ expires_at = datetime .datetime .fromtimestamp (introspected_token .exp )
69+ _introspection_token_cache [token_hash ] = _CachedToken (
70+ introspected_token = introspected_token ,
71+ expires_at = expires_at ,
72+ )
73+
74+
2575def verify_kiwix_access_token (token : str ) -> IntrospectedOAuth2Token :
26- """Verify a Kiwix access token by calling introspection endpoint."""
76+ """Verify a Kiwix access token by calling introspection endpoint.
77+
78+ Results are cached based on token expiration time to reduce API calls.
79+ """
80+ # Check cache first
81+ if cached_token := _get_cached_introspection_token (token ):
82+ return cached_token
83+
84+ # Cache miss - perform introspection
2785 with OryApiClient (_ory_client_configuration ) as api_client :
2886 api_instance = OryOAuth2Api (api_client )
2987 try :
@@ -37,6 +95,10 @@ def verify_kiwix_access_token(token: str) -> IntrospectedOAuth2Token:
3795 raise ValueError ("Kiwix access token issuer is not valid" )
3896 if KIWIX_CLIENT_ID != introspected_token .client_id :
3997 raise ValueError ("Kiwix access token client ID is not valid" )
98+
99+ # Cache the successful introspection result
100+ _cache_introspection_token (token , introspected_token )
101+
40102 return introspected_token
41103
42104
0 commit comments