rp_python_sdk 1.1.7__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/PKG-INFO +31 -2
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/README.md +30 -1
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/pyproject.toml +2 -2
- rp_python_sdk-1.2.0/rp_python_sdk/cache/__init__.py +0 -0
- rp_python_sdk-1.2.0/rp_python_sdk/cache/http_cache.py +117 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/get_participants.py +43 -15
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +5 -3
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/retrieve_tokens.py +23 -8
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/user_info.py +4 -3
- rp_python_sdk-1.2.0/rp_python_sdk/endpoints/util/version_info.py +2 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/relying_party_client_sdk.py +6 -4
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/sdk_config.py +4 -0
- rp_python_sdk-1.1.7/rp_python_sdk/endpoints/util/version_info.py +0 -2
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/__init__.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/mapping.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/system_info.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/filters/participant_filters.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/model.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/setup_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rp_python_sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: Erik Pragt
|
|
@@ -203,7 +203,12 @@ client = RelyingPartyClientSdk(
|
|
|
203
203
|
client_id='https://rp.directory.sandbox.connectid.com.au/openid_relying_party/280518db-9807-4824-b080-324d94b45f6a',
|
|
204
204
|
signing_kid='1X8udt28NY8NiSS8OJFvabv63K5igyFx5pM5ajKlMx8',
|
|
205
205
|
custom_config=CustomConfig(
|
|
206
|
-
enable_auto_compliance_verification=False
|
|
206
|
+
enable_auto_compliance_verification=False,
|
|
207
|
+
# HTTP cache configuration (optional)
|
|
208
|
+
cache_enabled=True, # Enable/disable caching (default: True)
|
|
209
|
+
cache_ttl_minutes=10, # Cache TTL in minutes (default: 10)
|
|
210
|
+
cache_max_entries=100, # Max cached entries (default: 100)
|
|
211
|
+
cache_max_element_size_bytes=5242880 # Max size per entry in bytes (default: 5MB)
|
|
207
212
|
)
|
|
208
213
|
),
|
|
209
214
|
)
|
|
@@ -211,6 +216,19 @@ client = RelyingPartyClientSdk(
|
|
|
211
216
|
participants = client.get_participants()
|
|
212
217
|
```
|
|
213
218
|
|
|
219
|
+
### HTTP Response Caching
|
|
220
|
+
|
|
221
|
+
The SDK includes an in-memory LRU cache that automatically caches HTTP responses for stable endpoints (participants registry,
|
|
222
|
+
OIDC discovery documents, and JWKS endpoints) to improve performance and reduce network calls. The cache is enabled by default
|
|
223
|
+
with sensible defaults, but can be customized or disabled via the `CustomConfig` options shown above.
|
|
224
|
+
|
|
225
|
+
Key cache features:
|
|
226
|
+
- **Thread-safe**: Safe for concurrent use across multiple threads
|
|
227
|
+
- **LRU eviction**: Automatically evicts least recently used entries when the cache is full
|
|
228
|
+
- **TTL expiration**: Entries expire after the configured TTL (default: 10 minutes)
|
|
229
|
+
- **Size limits**: Prevents caching of responses that exceed the configured size limit
|
|
230
|
+
- **Transparent**: Caching is automatic and transparent to SDK users
|
|
231
|
+
|
|
214
232
|
The response will contain List of Organisations and their Authorisation Server, with an object structure similar to below.
|
|
215
233
|
|
|
216
234
|
They key fields of interest are:
|
|
@@ -433,6 +451,17 @@ poetry publish
|
|
|
433
451
|
|
|
434
452
|
## Release Notes
|
|
435
453
|
|
|
454
|
+
### 1.2.0 (Feb 11, 2026)
|
|
455
|
+
- **New Feature: HTTP Response Cache** - Added in-memory LRU cache for HTTP responses to improve SDK performance
|
|
456
|
+
- Caches participants registry, OIDC discovery documents, and JWKS endpoints
|
|
457
|
+
- Configurable TTL (default: 10 minutes), max entries (default: 100), and size limits (default: 5MB per entry)
|
|
458
|
+
- Thread-safe with automatic LRU eviction
|
|
459
|
+
- Cache can be disabled or customized via `CustomConfig`:
|
|
460
|
+
- `cache_enabled` (bool, default: `True`)
|
|
461
|
+
- `cache_ttl_minutes` (int, default: `10`)
|
|
462
|
+
- `cache_max_entries` (int, default: `100`)
|
|
463
|
+
- `cache_max_element_size_bytes` (int, default: `5242880`)
|
|
464
|
+
|
|
436
465
|
### 1.1.7 (Jan 9, 2026)
|
|
437
466
|
- Security fix: Updated urllib3 to 2.6.3 to address high severity vulnerability (CVE-2026-21441)
|
|
438
467
|
|
|
@@ -182,7 +182,12 @@ client = RelyingPartyClientSdk(
|
|
|
182
182
|
client_id='https://rp.directory.sandbox.connectid.com.au/openid_relying_party/280518db-9807-4824-b080-324d94b45f6a',
|
|
183
183
|
signing_kid='1X8udt28NY8NiSS8OJFvabv63K5igyFx5pM5ajKlMx8',
|
|
184
184
|
custom_config=CustomConfig(
|
|
185
|
-
enable_auto_compliance_verification=False
|
|
185
|
+
enable_auto_compliance_verification=False,
|
|
186
|
+
# HTTP cache configuration (optional)
|
|
187
|
+
cache_enabled=True, # Enable/disable caching (default: True)
|
|
188
|
+
cache_ttl_minutes=10, # Cache TTL in minutes (default: 10)
|
|
189
|
+
cache_max_entries=100, # Max cached entries (default: 100)
|
|
190
|
+
cache_max_element_size_bytes=5242880 # Max size per entry in bytes (default: 5MB)
|
|
186
191
|
)
|
|
187
192
|
),
|
|
188
193
|
)
|
|
@@ -190,6 +195,19 @@ client = RelyingPartyClientSdk(
|
|
|
190
195
|
participants = client.get_participants()
|
|
191
196
|
```
|
|
192
197
|
|
|
198
|
+
### HTTP Response Caching
|
|
199
|
+
|
|
200
|
+
The SDK includes an in-memory LRU cache that automatically caches HTTP responses for stable endpoints (participants registry,
|
|
201
|
+
OIDC discovery documents, and JWKS endpoints) to improve performance and reduce network calls. The cache is enabled by default
|
|
202
|
+
with sensible defaults, but can be customized or disabled via the `CustomConfig` options shown above.
|
|
203
|
+
|
|
204
|
+
Key cache features:
|
|
205
|
+
- **Thread-safe**: Safe for concurrent use across multiple threads
|
|
206
|
+
- **LRU eviction**: Automatically evicts least recently used entries when the cache is full
|
|
207
|
+
- **TTL expiration**: Entries expire after the configured TTL (default: 10 minutes)
|
|
208
|
+
- **Size limits**: Prevents caching of responses that exceed the configured size limit
|
|
209
|
+
- **Transparent**: Caching is automatic and transparent to SDK users
|
|
210
|
+
|
|
193
211
|
The response will contain List of Organisations and their Authorisation Server, with an object structure similar to below.
|
|
194
212
|
|
|
195
213
|
They key fields of interest are:
|
|
@@ -412,6 +430,17 @@ poetry publish
|
|
|
412
430
|
|
|
413
431
|
## Release Notes
|
|
414
432
|
|
|
433
|
+
### 1.2.0 (Feb 11, 2026)
|
|
434
|
+
- **New Feature: HTTP Response Cache** - Added in-memory LRU cache for HTTP responses to improve SDK performance
|
|
435
|
+
- Caches participants registry, OIDC discovery documents, and JWKS endpoints
|
|
436
|
+
- Configurable TTL (default: 10 minutes), max entries (default: 100), and size limits (default: 5MB per entry)
|
|
437
|
+
- Thread-safe with automatic LRU eviction
|
|
438
|
+
- Cache can be disabled or customized via `CustomConfig`:
|
|
439
|
+
- `cache_enabled` (bool, default: `True`)
|
|
440
|
+
- `cache_ttl_minutes` (int, default: `10`)
|
|
441
|
+
- `cache_max_entries` (int, default: `100`)
|
|
442
|
+
- `cache_max_element_size_bytes` (int, default: `5242880`)
|
|
443
|
+
|
|
415
444
|
### 1.1.7 (Jan 9, 2026)
|
|
416
445
|
- Security fix: Updated urllib3 to 2.6.3 to address high severity vulnerability (CVE-2026-21441)
|
|
417
446
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "rp_python_sdk"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem"
|
|
5
5
|
authors = ["Erik Pragt <erik.pragt@connectid.com.au>"]
|
|
6
6
|
license = "Proprietary"
|
|
@@ -21,7 +21,7 @@ httpx = "^0.28.1"
|
|
|
21
21
|
toml = "^0.10.2"
|
|
22
22
|
|
|
23
23
|
[tool.poetry.group.dev.dependencies]
|
|
24
|
-
black = "^
|
|
24
|
+
black = "^26.0.0"
|
|
25
25
|
pre-commit = "^4.5.1"
|
|
26
26
|
mypy = "^1.19.1"
|
|
27
27
|
requests-mock = "^1.12.1"
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from threading import Lock
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from rp_python_sdk.sdk_config import CustomConfig
|
|
7
|
+
|
|
8
|
+
from rp_python_sdk.setup_logger import logger
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CacheEntry:
|
|
12
|
+
content: str
|
|
13
|
+
fetched_at: datetime
|
|
14
|
+
size_bytes: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpCache:
|
|
18
|
+
def __init__(self, config: CustomConfig) -> None:
|
|
19
|
+
self._enabled = config.cache_enabled
|
|
20
|
+
self._ttl_minutes = config.cache_ttl_minutes
|
|
21
|
+
self._max_entries = config.cache_max_entries
|
|
22
|
+
self._max_element_size_bytes = config.cache_max_element_size_bytes
|
|
23
|
+
|
|
24
|
+
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
25
|
+
self._lock = Lock()
|
|
26
|
+
|
|
27
|
+
logger.debug(
|
|
28
|
+
f"HTTP cache initialized: enabled={self._enabled}, ttlMinutes={self._ttl_minutes}, "
|
|
29
|
+
f"maxEntries={self._max_entries}, maxElementSize={self._max_element_size_bytes}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def get(self, url: str) -> Optional[str]:
|
|
33
|
+
if not self._enabled:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
with self._lock:
|
|
37
|
+
if url not in self._cache:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
entry = self._cache[url]
|
|
41
|
+
|
|
42
|
+
if self._is_stale_entry(entry):
|
|
43
|
+
logger.debug(f"Cache entry stale for URL: {url}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
self._cache.move_to_end(url)
|
|
47
|
+
logger.debug(f"Cache hit for URL: {url}")
|
|
48
|
+
return entry.content
|
|
49
|
+
|
|
50
|
+
def put(self, url: str, content: str) -> None:
|
|
51
|
+
if not self._enabled:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
size_bytes = len(content.encode('utf-8'))
|
|
55
|
+
|
|
56
|
+
if size_bytes > self._max_element_size_bytes:
|
|
57
|
+
logger.warning(
|
|
58
|
+
f"Response too large to cache: {size_bytes} bytes (max: {self._max_element_size_bytes}), URL: {url}"
|
|
59
|
+
)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
with self._lock:
|
|
63
|
+
entry = CacheEntry(
|
|
64
|
+
content=content,
|
|
65
|
+
fetched_at=datetime.now(timezone.utc),
|
|
66
|
+
size_bytes=size_bytes
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self._cache[url] = entry
|
|
70
|
+
self._cache.move_to_end(url)
|
|
71
|
+
|
|
72
|
+
logger.debug(f"Cached response for URL: {url}, size: {size_bytes} bytes")
|
|
73
|
+
|
|
74
|
+
if len(self._cache) > self._max_entries:
|
|
75
|
+
evicted_url, _ = self._cache.popitem(last=False)
|
|
76
|
+
logger.debug(f"Evicted cache entry for URL: {evicted_url}")
|
|
77
|
+
|
|
78
|
+
def is_stale(self, url: str) -> bool:
|
|
79
|
+
if not self._enabled:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
with self._lock:
|
|
83
|
+
if url not in self._cache:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
entry = self._cache[url]
|
|
87
|
+
return self._is_stale_entry(entry)
|
|
88
|
+
|
|
89
|
+
def evict(self, url: str) -> None:
|
|
90
|
+
if not self._enabled:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
with self._lock:
|
|
94
|
+
if url in self._cache:
|
|
95
|
+
del self._cache[url]
|
|
96
|
+
logger.debug(f"Evicted cache entry for URL: {url}")
|
|
97
|
+
|
|
98
|
+
def clear(self) -> None:
|
|
99
|
+
if not self._enabled:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
with self._lock:
|
|
103
|
+
self._cache.clear()
|
|
104
|
+
logger.debug("Cache cleared")
|
|
105
|
+
|
|
106
|
+
def size(self) -> int:
|
|
107
|
+
with self._lock:
|
|
108
|
+
return len(self._cache)
|
|
109
|
+
|
|
110
|
+
def is_enabled(self) -> bool:
|
|
111
|
+
return self._enabled
|
|
112
|
+
|
|
113
|
+
def _is_stale_entry(self, entry: CacheEntry) -> bool:
|
|
114
|
+
expiry_time: datetime = entry.fetched_at + timedelta(minutes=self._ttl_minutes)
|
|
115
|
+
current_time: datetime = datetime.now(timezone.utc)
|
|
116
|
+
is_stale: bool = current_time >= expiry_time
|
|
117
|
+
return is_stale
|
|
@@ -14,10 +14,11 @@ from rp_python_sdk.model import Participant
|
|
|
14
14
|
from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
|
|
15
15
|
from rp_python_sdk.sdk_config import SdkConfig
|
|
16
16
|
from rp_python_sdk.setup_logger import logger
|
|
17
|
+
from rp_python_sdk.cache.http_cache import HttpCache
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def get_participants(config: SdkConfig) -> list[Participant]:
|
|
20
|
-
participants = _retrieve_full_participants_list(config)
|
|
20
|
+
def get_participants(config: SdkConfig, cache: HttpCache) -> list[Participant]:
|
|
21
|
+
participants = _retrieve_full_participants_list(config, cache)
|
|
21
22
|
|
|
22
23
|
filtered_participants = remove_fallback_identity_service_provider(participants)
|
|
23
24
|
|
|
@@ -47,8 +48,8 @@ def get_participants(config: SdkConfig) -> list[Participant]:
|
|
|
47
48
|
return filtered_participants
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def retrieve_fallback_provider_participants(config: SdkConfig) -> list[Participant]:
|
|
51
|
-
participants = _retrieve_full_participants_list(config)
|
|
51
|
+
def retrieve_fallback_provider_participants(config: SdkConfig, cache: HttpCache) -> list[Participant]:
|
|
52
|
+
participants = _retrieve_full_participants_list(config, cache)
|
|
52
53
|
|
|
53
54
|
filtered_participants = remove_out_of_date_certifications(participants, date.today())
|
|
54
55
|
filtered_participants = remove_unofficial_certifications(filtered_participants)
|
|
@@ -59,10 +60,21 @@ def retrieve_fallback_provider_participants(config: SdkConfig) -> list[Participa
|
|
|
59
60
|
return filtered_participants
|
|
60
61
|
|
|
61
62
|
|
|
62
|
-
def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
|
|
63
|
-
|
|
63
|
+
def _retrieve_full_participants_list(config: SdkConfig, cache: HttpCache) -> list[Participant]:
|
|
64
|
+
url = config.registry_participants_uri
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
# Try cache first
|
|
67
|
+
cached = cache.get(url)
|
|
68
|
+
if cached:
|
|
69
|
+
logger.debug(f"Cache hit for URL: {url}")
|
|
70
|
+
participant_list: list[Participant] = from_dict(Participant, json.loads(cached), pascal_case=True)
|
|
71
|
+
logger.info(f"Retrieved identity providers from cache, num orgs found: {len(participant_list)}")
|
|
72
|
+
return participant_list
|
|
73
|
+
|
|
74
|
+
logger.debug(f"Cache miss for URL: {url}")
|
|
75
|
+
logger.info(f"Retrieving all identity providers from {url}")
|
|
76
|
+
|
|
77
|
+
response = requests.get(url,
|
|
66
78
|
timeout=config.custom_config.timeout_in_seconds,
|
|
67
79
|
headers={
|
|
68
80
|
'User-Agent': build_user_agent(config.client_id)
|
|
@@ -71,6 +83,10 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
|
|
|
71
83
|
raise RelyingPartyClientSdkException(
|
|
72
84
|
f"Call to identity provider endpoint failed with code: {response.status_code}")
|
|
73
85
|
|
|
86
|
+
# Cache the successful response
|
|
87
|
+
cache.put(url, response.text)
|
|
88
|
+
logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
|
|
89
|
+
|
|
74
90
|
# participant_list: list[Participant] = Participant.from_json(response.text)
|
|
75
91
|
participant_list: list[Participant] = from_dict(Participant, json.loads(response.text), pascal_case=True)
|
|
76
92
|
|
|
@@ -78,7 +94,7 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
|
|
|
78
94
|
return participant_list
|
|
79
95
|
|
|
80
96
|
|
|
81
|
-
def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) -> AuthorisationServer:
|
|
97
|
+
def get_auth_server_details(config: SdkConfig, authorisation_server_id: str, cache: HttpCache) -> AuthorisationServer:
|
|
82
98
|
def find_auth_server_by_id(participants: list[Participant], target_id: str) -> AuthorisationServer | None:
|
|
83
99
|
for participant in participants:
|
|
84
100
|
for server in participant.authorisation_servers:
|
|
@@ -87,11 +103,11 @@ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) ->
|
|
|
87
103
|
return None
|
|
88
104
|
|
|
89
105
|
# Search in main participants
|
|
90
|
-
result = find_auth_server_by_id(get_participants(config), authorisation_server_id)
|
|
106
|
+
result = find_auth_server_by_id(get_participants(config, cache), authorisation_server_id)
|
|
91
107
|
|
|
92
108
|
# If not found, check fallback provider participants
|
|
93
109
|
if result is None:
|
|
94
|
-
fallback_participants = retrieve_fallback_provider_participants(config)
|
|
110
|
+
fallback_participants = retrieve_fallback_provider_participants(config, cache)
|
|
95
111
|
result = find_auth_server_by_id(fallback_participants, authorisation_server_id)
|
|
96
112
|
|
|
97
113
|
if result is None:
|
|
@@ -101,14 +117,26 @@ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) ->
|
|
|
101
117
|
return result
|
|
102
118
|
|
|
103
119
|
|
|
104
|
-
def get_issuer_metadata(authorisation_server: AuthorisationServer) -> IssuerMetadata:
|
|
105
|
-
|
|
120
|
+
def get_issuer_metadata(authorisation_server: AuthorisationServer, cache: HttpCache) -> IssuerMetadata:
|
|
121
|
+
url = authorisation_server.open_i_d_discovery_document
|
|
122
|
+
|
|
123
|
+
# Try cache first
|
|
124
|
+
cached = cache.get(url)
|
|
125
|
+
if cached:
|
|
126
|
+
logger.debug(f"Cache hit for URL: {url}")
|
|
127
|
+
return from_dict(IssuerMetadata, json.loads(cached))
|
|
128
|
+
|
|
129
|
+
logger.debug(f"Cache miss for URL: {url}")
|
|
106
130
|
|
|
107
131
|
try:
|
|
108
|
-
logger.info(f"Getting discovery document from {
|
|
109
|
-
response = requests.get(
|
|
132
|
+
logger.info(f"Getting discovery document from {url}")
|
|
133
|
+
response = requests.get(url)
|
|
110
134
|
response.raise_for_status() # Raises an exception for 4XX or 5XX errors
|
|
111
135
|
|
|
136
|
+
# Cache the successful response
|
|
137
|
+
cache.put(url, response.text)
|
|
138
|
+
logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
|
|
139
|
+
|
|
112
140
|
return from_dict(IssuerMetadata, json.loads(response.text))
|
|
113
141
|
|
|
114
142
|
except requests.exceptions.HTTPError as http_err:
|
|
@@ -116,4 +144,4 @@ def get_issuer_metadata(authorisation_server: AuthorisationServer) -> IssuerMeta
|
|
|
116
144
|
f"Call to identity provider endpoint failed with code: {response.status_code}") from http_err
|
|
117
145
|
except requests.exceptions.RequestException as req_err:
|
|
118
146
|
raise RelyingPartyClientSdkException(
|
|
119
|
-
f"Failed to connect to authorisation service {
|
|
147
|
+
f"Failed to connect to authorisation service {url}") from req_err
|
{rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py
RENAMED
|
@@ -16,6 +16,7 @@ from rp_python_sdk.model import PARResponse
|
|
|
16
16
|
from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
|
|
17
17
|
from rp_python_sdk.sdk_config import SdkConfig
|
|
18
18
|
from rp_python_sdk.setup_logger import logger
|
|
19
|
+
from rp_python_sdk.cache.http_cache import HttpCache
|
|
19
20
|
|
|
20
21
|
extended_claims = {"over16", "over18", "over21", "over25", "over65", "beneficiary_account_au",
|
|
21
22
|
"beneficiary_account_au_payid", "beneficiary_account_international"}
|
|
@@ -25,14 +26,15 @@ def send_pushed_authorisation_request(config: SdkConfig,
|
|
|
25
26
|
authorisation_server_id: str,
|
|
26
27
|
essential_claims: set[str],
|
|
27
28
|
voluntary_claims: set[str],
|
|
28
|
-
purpose: str
|
|
29
|
+
purpose: str,
|
|
30
|
+
cache: HttpCache) -> PARResponse:
|
|
29
31
|
_check_valid_purpose(purpose)
|
|
30
32
|
|
|
31
33
|
checked_essential_claims = _ensure_mandatory_claims_present(essential_claims, voluntary_claims)
|
|
32
34
|
claim_request = _generate_claims_request(checked_essential_claims, voluntary_claims)
|
|
33
35
|
|
|
34
|
-
authorisation_server = get_auth_server_details(config, authorisation_server_id)
|
|
35
|
-
issuer_metadata = get_issuer_metadata(authorisation_server)
|
|
36
|
+
authorisation_server = get_auth_server_details(config, authorisation_server_id, cache)
|
|
37
|
+
issuer_metadata = get_issuer_metadata(authorisation_server, cache)
|
|
36
38
|
|
|
37
39
|
auth_server_url = issuer_metadata.issuer
|
|
38
40
|
x_fapi_interaction_id = create_x_fapi_interaction_id()
|
|
@@ -22,12 +22,13 @@ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientS
|
|
|
22
22
|
from rp_python_sdk.sdk_config import SdkConfig
|
|
23
23
|
from rp_python_sdk.setup_logger import logger
|
|
24
24
|
|
|
25
|
+
from rp_python_sdk.cache.http_cache import HttpCache
|
|
25
26
|
|
|
26
27
|
def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_body: CallbackBody,
|
|
27
28
|
original_code_verifier: str,
|
|
28
|
-
original_state: str, nonce: str) -> TokenSet:
|
|
29
|
-
authorisation_server = get_auth_server_details(config, authorisation_server_id)
|
|
30
|
-
issuer_metadata = get_issuer_metadata(authorisation_server)
|
|
29
|
+
original_state: str, nonce: str, cache: HttpCache) -> TokenSet:
|
|
30
|
+
authorisation_server = get_auth_server_details(config, authorisation_server_id, cache)
|
|
31
|
+
issuer_metadata = get_issuer_metadata(authorisation_server, cache)
|
|
31
32
|
|
|
32
33
|
if issuer_metadata.issuer != callback_body.iss:
|
|
33
34
|
raise RelyingPartyClientSdkException(
|
|
@@ -104,7 +105,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
104
105
|
|
|
105
106
|
token_input = from_dict(TokenInput, json.loads(response.text))
|
|
106
107
|
|
|
107
|
-
jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
|
|
108
|
+
jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id, cache)
|
|
108
109
|
allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
|
|
109
110
|
|
|
110
111
|
header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body,
|
|
@@ -113,7 +114,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
113
114
|
token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
|
|
114
115
|
|
|
115
116
|
if config.custom_config.enable_auto_compliance_verification:
|
|
116
|
-
call_user_info(config, authorisation_server_id, token_set.token_input.access_token)
|
|
117
|
+
call_user_info(config, authorisation_server_id, token_set.token_input.access_token, cache)
|
|
117
118
|
|
|
118
119
|
logger.info(
|
|
119
120
|
f"Token response successfully retrieved from authorisation server {authorisation_server_id}, "
|
|
@@ -275,17 +276,31 @@ def _matching_alg(token_alg: str, key_element: dict) -> bool:
|
|
|
275
276
|
return key_element.get("alg") is None or key_element.get("alg") == token_alg
|
|
276
277
|
|
|
277
278
|
|
|
278
|
-
def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
|
|
279
|
+
def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str, cache: HttpCache) -> Any:
|
|
280
|
+
url = issuer_metadata.jwks_uri
|
|
281
|
+
|
|
282
|
+
# Try cache first
|
|
283
|
+
cached = cache.get(url)
|
|
284
|
+
if cached:
|
|
285
|
+
logger.debug(f"Cache hit for URL: {url}")
|
|
286
|
+
return json.loads(cached)
|
|
287
|
+
|
|
288
|
+
logger.debug(f"Cache miss for URL: {url}")
|
|
289
|
+
|
|
279
290
|
headers = {
|
|
280
291
|
"Accept": "application/json",
|
|
281
292
|
"x-fapi-interaction-id": x_fapi_interaction_id
|
|
282
293
|
}
|
|
283
|
-
response = requests.get(
|
|
294
|
+
response = requests.get(url, headers=headers)
|
|
284
295
|
response.raise_for_status()
|
|
285
296
|
|
|
286
297
|
if not response.ok:
|
|
287
298
|
raise RelyingPartyClientSdkException(
|
|
288
|
-
f"Response code for JWKS to {
|
|
299
|
+
f"Response code for JWKS to {url} not successful, was: {response.status_code}, "
|
|
289
300
|
f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
|
|
290
301
|
|
|
302
|
+
# Cache the successful response
|
|
303
|
+
cache.put(url, response.text)
|
|
304
|
+
logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
|
|
305
|
+
|
|
291
306
|
return response.json()
|
|
@@ -2,6 +2,7 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
4
|
|
|
5
|
+
from rp_python_sdk.cache.http_cache import HttpCache
|
|
5
6
|
from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
|
|
6
7
|
get_auth_server_details
|
|
7
8
|
from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id
|
|
@@ -10,12 +11,12 @@ from rp_python_sdk.sdk_config import SdkConfig
|
|
|
10
11
|
from rp_python_sdk.setup_logger import logger
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def call_user_info(config: SdkConfig, authorisation_server_id: str, access_token: str) -> dict[str, Any]:
|
|
14
|
+
def call_user_info(config: SdkConfig, authorisation_server_id: str, access_token: str, cache: HttpCache) -> dict[str, Any]:
|
|
14
15
|
x_fapi_interaction_id = create_x_fapi_interaction_id()
|
|
15
16
|
logger.info(f"Making call to user info endpoint, x-fapi-interaction-id: {x_fapi_interaction_id}")
|
|
16
17
|
|
|
17
|
-
authorisation_server = get_auth_server_details(config, authorisation_server_id)
|
|
18
|
-
issuer_metadata = get_issuer_metadata(authorisation_server)
|
|
18
|
+
authorisation_server = get_auth_server_details(config, authorisation_server_id, cache)
|
|
19
|
+
issuer_metadata = get_issuer_metadata(authorisation_server, cache)
|
|
19
20
|
|
|
20
21
|
user_info_endpoint = issuer_metadata.get_preferred_userinfo_endpoint()
|
|
21
22
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from rp_python_sdk.cache.http_cache import HttpCache
|
|
1
2
|
from rp_python_sdk.endpoints.get_participants import get_participants, \
|
|
2
3
|
retrieve_fallback_provider_participants
|
|
3
4
|
from rp_python_sdk.endpoints.pushed_authorisation_request import send_pushed_authorisation_request
|
|
@@ -12,6 +13,7 @@ class RelyingPartyClientSdk:
|
|
|
12
13
|
|
|
13
14
|
def __init__(self, config: SdkConfig):
|
|
14
15
|
self.config = config
|
|
16
|
+
self.cache = HttpCache(config.custom_config)
|
|
15
17
|
|
|
16
18
|
if config.custom_config.enable_auto_compliance_verification:
|
|
17
19
|
logger.info(f"Auto Compliance Verification mode enabled, printing config information: "
|
|
@@ -21,10 +23,10 @@ class RelyingPartyClientSdk:
|
|
|
21
23
|
f"registry_participants_uri={self.config.registry_participants_uri}")
|
|
22
24
|
|
|
23
25
|
def get_participants(self) -> list[Participant]:
|
|
24
|
-
return get_participants(self.config)
|
|
26
|
+
return get_participants(self.config, self.cache)
|
|
25
27
|
|
|
26
28
|
def get_fallback_provider_participants(self) -> list[Participant]:
|
|
27
|
-
return retrieve_fallback_provider_participants(self.config)
|
|
29
|
+
return retrieve_fallback_provider_participants(self.config, self.cache)
|
|
28
30
|
|
|
29
31
|
def send_pushed_authorisation_request(self, authorisation_server_id: str,
|
|
30
32
|
essential_claims: set[str] | None = None,
|
|
@@ -36,9 +38,9 @@ class RelyingPartyClientSdk:
|
|
|
36
38
|
checked_purpose = purpose if purpose else self.config.custom_config.purpose
|
|
37
39
|
|
|
38
40
|
return send_pushed_authorisation_request(self.config, authorisation_server_id, checked_essential_claims,
|
|
39
|
-
checked_voluntary_claims, checked_purpose)
|
|
41
|
+
checked_voluntary_claims, checked_purpose, self.cache)
|
|
40
42
|
|
|
41
43
|
def retrieve_tokens(self, authorisation_server_id: str, callback_body: CallbackBody, original_code_verifier: str,
|
|
42
44
|
original_state: str, nonce: str) -> TokenSet:
|
|
43
45
|
return retrieve_tokens(self.config, authorisation_server_id, callback_body, original_code_verifier,
|
|
44
|
-
original_state, nonce)
|
|
46
|
+
original_state, nonce, self.cache)
|
|
@@ -23,6 +23,10 @@ class CustomConfig:
|
|
|
23
23
|
required_claims: List[str] = field(default_factory=list)
|
|
24
24
|
required_participant_certifications: List['CertificationFilter'] = field(default_factory=list)
|
|
25
25
|
timeout_in_seconds: int = 10
|
|
26
|
+
cache_enabled: bool = True
|
|
27
|
+
cache_ttl_minutes: int = 10
|
|
28
|
+
cache_max_entries: int = 100
|
|
29
|
+
cache_max_element_size_bytes: int = 5242880
|
|
26
30
|
|
|
27
31
|
def __post_init__(self) -> None:
|
|
28
32
|
if self.include_uncertified_participants:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rp_python_sdk-1.1.7 → rp_python_sdk-1.2.0}/rp_python_sdk/relying_party_client_sdk_exception.py
RENAMED
|
File without changes
|
|
File without changes
|