rp_python_sdk 1.1.7__tar.gz → 1.3.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/README.md → rp_python_sdk-1.3.0/PKG-INFO +59 -1
- rp_python_sdk-1.1.7/PKG-INFO → rp_python_sdk-1.3.0/README.md +37 -23
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/pyproject.toml +4 -4
- rp_python_sdk-1.3.0/rp_python_sdk/cache/__init__.py +0 -0
- rp_python_sdk-1.3.0/rp_python_sdk/cache/http_cache.py +117 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/get_participants.py +59 -16
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +5 -3
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/retrieve_tokens.py +23 -8
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/user_info.py +4 -3
- rp_python_sdk-1.3.0/rp_python_sdk/endpoints/util/version_info.py +2 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/relying_party_client_sdk.py +6 -4
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.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.3.0}/rp_python_sdk/__init__.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/util/mapping.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/util/system_info.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/endpoints/util/user_agent.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/filters/participant_filters.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/model.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
- {rp_python_sdk-1.1.7 → rp_python_sdk-1.3.0}/rp_python_sdk/setup_logger.py +0 -0
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rp_python_sdk
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem
|
|
5
|
+
License: Proprietary
|
|
6
|
+
Author: Erik Pragt
|
|
7
|
+
Author-email: erik.pragt@connectid.com.au
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: authlib (>=1.6.6,<2.0.0)
|
|
15
|
+
Requires-Dist: cffi (>=2.0.0,<3.0.0)
|
|
16
|
+
Requires-Dist: cryptography (>=46.0.5,<47.0.0)
|
|
17
|
+
Requires-Dist: joserfc (>=1.6.3,<2.0.0)
|
|
18
|
+
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
19
|
+
Requires-Dist: urllib3 (>=2.6.3,<3.0.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
1
22
|
# Python SDK for Relying Parties
|
|
2
23
|
|
|
3
24
|
[](https://github.com/connectid-tools/rp-python-sdk/actions/workflows/build.yml)
|
|
@@ -182,7 +203,12 @@ client = RelyingPartyClientSdk(
|
|
|
182
203
|
client_id='https://rp.directory.sandbox.connectid.com.au/openid_relying_party/280518db-9807-4824-b080-324d94b45f6a',
|
|
183
204
|
signing_kid='1X8udt28NY8NiSS8OJFvabv63K5igyFx5pM5ajKlMx8',
|
|
184
205
|
custom_config=CustomConfig(
|
|
185
|
-
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)
|
|
186
212
|
)
|
|
187
213
|
),
|
|
188
214
|
)
|
|
@@ -190,6 +216,19 @@ client = RelyingPartyClientSdk(
|
|
|
190
216
|
participants = client.get_participants()
|
|
191
217
|
```
|
|
192
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
|
+
|
|
193
232
|
The response will contain List of Organisations and their Authorisation Server, with an object structure similar to below.
|
|
194
233
|
|
|
195
234
|
They key fields of interest are:
|
|
@@ -412,6 +451,24 @@ poetry publish
|
|
|
412
451
|
|
|
413
452
|
## Release Notes
|
|
414
453
|
|
|
454
|
+
## 1.3.0 (23 Mar, 2026)
|
|
455
|
+
- **OpenID Connect Discovery Section 4.3 Compliance**: Implemented issuer validation to ensure the issuer value in the provider configuration matches the Issuer URL used to retrieve it
|
|
456
|
+
- Added validation that compares the issuer in `/.well-known/openid-configuration` response with the expected issuer URL
|
|
457
|
+
- Properly handles trailing slash normalisation per RFC 3986
|
|
458
|
+
- Throws descriptive exception when issuer mismatch is detected, stopping the flow as required by the specification
|
|
459
|
+
- Validates both fresh and cached issuer metadata
|
|
460
|
+
|
|
461
|
+
### 1.2.0 (Feb 11, 2026)
|
|
462
|
+
- **New Feature: HTTP Response Cache** - Added in-memory LRU cache for HTTP responses to improve SDK performance
|
|
463
|
+
- Caches participants registry, OIDC discovery documents, and JWKS endpoints
|
|
464
|
+
- Configurable TTL (default: 10 minutes), max entries (default: 100), and size limits (default: 5MB per entry)
|
|
465
|
+
- Thread-safe with automatic LRU eviction
|
|
466
|
+
- Cache can be disabled or customized via `CustomConfig`:
|
|
467
|
+
- `cache_enabled` (bool, default: `True`)
|
|
468
|
+
- `cache_ttl_minutes` (int, default: `10`)
|
|
469
|
+
- `cache_max_entries` (int, default: `100`)
|
|
470
|
+
- `cache_max_element_size_bytes` (int, default: `5242880`)
|
|
471
|
+
|
|
415
472
|
### 1.1.7 (Jan 9, 2026)
|
|
416
473
|
- Security fix: Updated urllib3 to 2.6.3 to address high severity vulnerability (CVE-2026-21441)
|
|
417
474
|
|
|
@@ -486,3 +543,4 @@ def send_pushed_authorisation_request(config: SdkConfig,
|
|
|
486
543
|
|
|
487
544
|
### 0.1.0 (March 27, 2024)
|
|
488
545
|
- Initial version
|
|
546
|
+
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: rp_python_sdk
|
|
3
|
-
Version: 1.1.7
|
|
4
|
-
Summary: Python SDK for Relying Parties to enable simple integration with the Digital Identity ecosystem
|
|
5
|
-
License: Proprietary
|
|
6
|
-
Author: Erik Pragt
|
|
7
|
-
Author-email: erik.pragt@connectid.com.au
|
|
8
|
-
Requires-Python: >=3.12,<4.0
|
|
9
|
-
Classifier: License :: Other/Proprietary License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
Requires-Dist: authlib (>=1.6.6,<2.0.0)
|
|
15
|
-
Requires-Dist: cffi (>=2.0.0,<3.0.0)
|
|
16
|
-
Requires-Dist: cryptography (>=46.0.2,<47.0.0)
|
|
17
|
-
Requires-Dist: joserfc (>=1.6.1,<2.0.0)
|
|
18
|
-
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
19
|
-
Requires-Dist: urllib3 (>=2.6.3,<3.0.0)
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
1
|
# Python SDK for Relying Parties
|
|
23
2
|
|
|
24
3
|
[](https://github.com/connectid-tools/rp-python-sdk/actions/workflows/build.yml)
|
|
@@ -203,7 +182,12 @@ client = RelyingPartyClientSdk(
|
|
|
203
182
|
client_id='https://rp.directory.sandbox.connectid.com.au/openid_relying_party/280518db-9807-4824-b080-324d94b45f6a',
|
|
204
183
|
signing_kid='1X8udt28NY8NiSS8OJFvabv63K5igyFx5pM5ajKlMx8',
|
|
205
184
|
custom_config=CustomConfig(
|
|
206
|
-
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)
|
|
207
191
|
)
|
|
208
192
|
),
|
|
209
193
|
)
|
|
@@ -211,6 +195,19 @@ client = RelyingPartyClientSdk(
|
|
|
211
195
|
participants = client.get_participants()
|
|
212
196
|
```
|
|
213
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
|
+
|
|
214
211
|
The response will contain List of Organisations and their Authorisation Server, with an object structure similar to below.
|
|
215
212
|
|
|
216
213
|
They key fields of interest are:
|
|
@@ -433,6 +430,24 @@ poetry publish
|
|
|
433
430
|
|
|
434
431
|
## Release Notes
|
|
435
432
|
|
|
433
|
+
## 1.3.0 (23 Mar, 2026)
|
|
434
|
+
- **OpenID Connect Discovery Section 4.3 Compliance**: Implemented issuer validation to ensure the issuer value in the provider configuration matches the Issuer URL used to retrieve it
|
|
435
|
+
- Added validation that compares the issuer in `/.well-known/openid-configuration` response with the expected issuer URL
|
|
436
|
+
- Properly handles trailing slash normalisation per RFC 3986
|
|
437
|
+
- Throws descriptive exception when issuer mismatch is detected, stopping the flow as required by the specification
|
|
438
|
+
- Validates both fresh and cached issuer metadata
|
|
439
|
+
|
|
440
|
+
### 1.2.0 (Feb 11, 2026)
|
|
441
|
+
- **New Feature: HTTP Response Cache** - Added in-memory LRU cache for HTTP responses to improve SDK performance
|
|
442
|
+
- Caches participants registry, OIDC discovery documents, and JWKS endpoints
|
|
443
|
+
- Configurable TTL (default: 10 minutes), max entries (default: 100), and size limits (default: 5MB per entry)
|
|
444
|
+
- Thread-safe with automatic LRU eviction
|
|
445
|
+
- Cache can be disabled or customized via `CustomConfig`:
|
|
446
|
+
- `cache_enabled` (bool, default: `True`)
|
|
447
|
+
- `cache_ttl_minutes` (int, default: `10`)
|
|
448
|
+
- `cache_max_entries` (int, default: `100`)
|
|
449
|
+
- `cache_max_element_size_bytes` (int, default: `5242880`)
|
|
450
|
+
|
|
436
451
|
### 1.1.7 (Jan 9, 2026)
|
|
437
452
|
- Security fix: Updated urllib3 to 2.6.3 to address high severity vulnerability (CVE-2026-21441)
|
|
438
453
|
|
|
@@ -507,4 +522,3 @@ def send_pushed_authorisation_request(config: SdkConfig,
|
|
|
507
522
|
|
|
508
523
|
### 0.1.0 (March 27, 2024)
|
|
509
524
|
- Initial version
|
|
510
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "rp_python_sdk"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.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"
|
|
@@ -10,8 +10,8 @@ readme = "README.md"
|
|
|
10
10
|
python = "^3.12"
|
|
11
11
|
requests = "^2.32.5"
|
|
12
12
|
authlib = "^1.6.6"
|
|
13
|
-
cryptography = "^46.0.
|
|
14
|
-
joserfc = "^1.6.
|
|
13
|
+
cryptography = "^46.0.5"
|
|
14
|
+
joserfc = "^1.6.3"
|
|
15
15
|
cffi = "^2.0.0"
|
|
16
16
|
urllib3 = "^2.6.3"
|
|
17
17
|
|
|
@@ -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,19 +117,46 @@ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) ->
|
|
|
101
117
|
return result
|
|
102
118
|
|
|
103
119
|
|
|
104
|
-
def
|
|
105
|
-
|
|
120
|
+
def _validate_issuer_match(discovery_url: str, actual_issuer: str) -> None:
|
|
121
|
+
well_known_suffix = "/.well-known/openid-configuration"
|
|
122
|
+
if well_known_suffix not in discovery_url or not actual_issuer:
|
|
123
|
+
return
|
|
124
|
+
expected_issuer = discovery_url[:discovery_url.index(well_known_suffix)]
|
|
125
|
+
if expected_issuer.rstrip("/") != actual_issuer.rstrip("/"):
|
|
126
|
+
raise RelyingPartyClientSdkException(
|
|
127
|
+
f"Issuer mismatch: discovery URL implies issuer '{expected_issuer}' "
|
|
128
|
+
f"but discovery document contains issuer '{actual_issuer}'")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_issuer_metadata(authorisation_server: AuthorisationServer, cache: HttpCache) -> IssuerMetadata:
|
|
132
|
+
url = authorisation_server.open_i_d_discovery_document
|
|
133
|
+
|
|
134
|
+
# Try cache first
|
|
135
|
+
cached = cache.get(url)
|
|
136
|
+
if cached:
|
|
137
|
+
logger.debug(f"Cache hit for URL: {url}")
|
|
138
|
+
metadata = from_dict(IssuerMetadata, json.loads(cached))
|
|
139
|
+
_validate_issuer_match(url, metadata.issuer)
|
|
140
|
+
return metadata
|
|
141
|
+
|
|
142
|
+
logger.debug(f"Cache miss for URL: {url}")
|
|
106
143
|
|
|
107
144
|
try:
|
|
108
|
-
logger.info(f"Getting discovery document from {
|
|
109
|
-
response = requests.get(
|
|
145
|
+
logger.info(f"Getting discovery document from {url}")
|
|
146
|
+
response = requests.get(url)
|
|
110
147
|
response.raise_for_status() # Raises an exception for 4XX or 5XX errors
|
|
111
148
|
|
|
112
|
-
|
|
149
|
+
# Cache the successful response
|
|
150
|
+
cache.put(url, response.text)
|
|
151
|
+
logger.debug(f"Cached response for URL: {url}, size: {len(response.text)} bytes")
|
|
152
|
+
|
|
153
|
+
metadata = from_dict(IssuerMetadata, json.loads(response.text))
|
|
154
|
+
_validate_issuer_match(url, metadata.issuer)
|
|
155
|
+
return metadata
|
|
113
156
|
|
|
114
157
|
except requests.exceptions.HTTPError as http_err:
|
|
115
158
|
raise RelyingPartyClientSdkException(
|
|
116
159
|
f"Call to identity provider endpoint failed with code: {response.status_code}") from http_err
|
|
117
160
|
except requests.exceptions.RequestException as req_err:
|
|
118
161
|
raise RelyingPartyClientSdkException(
|
|
119
|
-
f"Failed to connect to authorisation service {
|
|
162
|
+
f"Failed to connect to authorisation service {url}") from req_err
|
{rp_python_sdk-1.1.7 → rp_python_sdk-1.3.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.3.0}/rp_python_sdk/relying_party_client_sdk_exception.py
RENAMED
|
File without changes
|
|
File without changes
|