rp_python_sdk 0.9.0__tar.gz → 0.11.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-0.9.0 → rp_python_sdk-0.11.0}/PKG-INFO +7 -1
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/README.md +6 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/pyproject.toml +2 -2
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/retrieve_tokens.py +59 -43
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/util/mapping.py +1 -1
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/__init__.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/get_participants.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/user_info.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/filters/participant_filters.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/model.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/relying_party_client_sdk.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/sdk_config.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/setup_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rp_python_sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -426,6 +426,12 @@ poetry publish
|
|
|
426
426
|
|
|
427
427
|
## Release Notes
|
|
428
428
|
|
|
429
|
+
### 0.11.0 (Nov 29, 2024)
|
|
430
|
+
- Update to use Authorisation Server issuer attribute as `aud` value when retrieving tokens.
|
|
431
|
+
|
|
432
|
+
### 0.10.0 (Oct 24, 2024)
|
|
433
|
+
- JWT signature verification for id_token in the retrieve_tokens step.
|
|
434
|
+
|
|
429
435
|
### 0.9.0 (Oct 18, 2024)
|
|
430
436
|
- Added support for Python 3.13
|
|
431
437
|
- Added support for `payment` ApiResources
|
|
@@ -407,6 +407,12 @@ poetry publish
|
|
|
407
407
|
|
|
408
408
|
## Release Notes
|
|
409
409
|
|
|
410
|
+
### 0.11.0 (Nov 29, 2024)
|
|
411
|
+
- Update to use Authorisation Server issuer attribute as `aud` value when retrieving tokens.
|
|
412
|
+
|
|
413
|
+
### 0.10.0 (Oct 24, 2024)
|
|
414
|
+
- JWT signature verification for id_token in the retrieve_tokens step.
|
|
415
|
+
|
|
410
416
|
### 0.9.0 (Oct 18, 2024)
|
|
411
417
|
- Added support for Python 3.13
|
|
412
418
|
- Added support for `payment` ApiResources
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "rp_python_sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.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.27.2"
|
|
|
21
21
|
[tool.poetry.group.dev.dependencies]
|
|
22
22
|
black = "^24.10.0"
|
|
23
23
|
pre-commit = "^3.8.0"
|
|
24
|
-
mypy = "^1.
|
|
24
|
+
mypy = "^1.13.0"
|
|
25
25
|
requests-mock = "^1.12.1"
|
|
26
26
|
freezegun = "^1.4.0"
|
|
27
27
|
flake8 = "^7.1.1"
|
|
@@ -8,6 +8,9 @@ import requests
|
|
|
8
8
|
from joserfc import jws
|
|
9
9
|
from joserfc.jwk import RSAKey
|
|
10
10
|
from joserfc.jws import JWSRegistry
|
|
11
|
+
from authlib.jose import JsonWebToken
|
|
12
|
+
from authlib.jose.errors import BadSignatureError, DecodeError
|
|
13
|
+
from authlib.jose.util import extract_header
|
|
11
14
|
|
|
12
15
|
from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
|
|
13
16
|
get_auth_server_details
|
|
@@ -37,15 +40,16 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
37
40
|
f"{authorisation_server.customer_friendly_name}, x-fapi-interaction-id: {x_fapi_interaction_id}")
|
|
38
41
|
|
|
39
42
|
token_endpoint = issuer_metadata.get_preferred_token_endpoint()
|
|
43
|
+
issuer = issuer_metadata.issuer
|
|
40
44
|
|
|
41
45
|
if callback_body.state != original_state:
|
|
42
46
|
raise RelyingPartyClientSdkException(
|
|
43
47
|
f"Callback response state value does not match state, callback state: {callback_body.state}, "
|
|
44
48
|
f"state {original_state}, x-fapi-interaction-id: {x_fapi_interaction_id}")
|
|
45
49
|
|
|
46
|
-
if callback_body.iss !=
|
|
50
|
+
if callback_body.iss != issuer:
|
|
47
51
|
raise RelyingPartyClientSdkException(
|
|
48
|
-
f"iss mismatch, expected {
|
|
52
|
+
f"iss mismatch, expected {issuer}, got: {callback_body.iss}")
|
|
49
53
|
|
|
50
54
|
redirect_uri = config.application_redirect_uri
|
|
51
55
|
client_id = config.client_id
|
|
@@ -56,7 +60,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
56
60
|
|
|
57
61
|
client_assertion_payload = {
|
|
58
62
|
'sub': client_id,
|
|
59
|
-
'aud':
|
|
63
|
+
'aud': issuer,
|
|
60
64
|
'iss': client_id,
|
|
61
65
|
'exp': exp,
|
|
62
66
|
'jti': jti,
|
|
@@ -103,7 +107,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
103
107
|
jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
|
|
104
108
|
allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
|
|
105
109
|
|
|
106
|
-
header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body)
|
|
110
|
+
header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body, issuer_metadata.issuer)
|
|
107
111
|
|
|
108
112
|
token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
|
|
109
113
|
|
|
@@ -119,7 +123,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
119
123
|
|
|
120
124
|
def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allowed_algs: list[str],
|
|
121
125
|
nonce: str, client_id: str,
|
|
122
|
-
callback_body: CallbackBody) -> Tuple[dict[str, Any], dict[str, Any]]:
|
|
126
|
+
callback_body: CallbackBody, issuer: str) -> Tuple[dict[str, Any], dict[str, Any]]:
|
|
123
127
|
"""
|
|
124
128
|
Decode and parse an ID token into JSON objects for header and payload.
|
|
125
129
|
|
|
@@ -129,18 +133,18 @@ def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allow
|
|
|
129
133
|
:param nonce
|
|
130
134
|
:param client_id
|
|
131
135
|
:param callback_body
|
|
136
|
+
:param issuer
|
|
132
137
|
:return: two dictionaries for header and payload
|
|
133
138
|
"""
|
|
134
139
|
id_token = token_input.id_token
|
|
135
|
-
|
|
136
|
-
header = _decode_base64(header_base64)
|
|
137
|
-
header_json = json.loads(header)
|
|
140
|
+
header_json = _get_unverified_header(id_token)
|
|
138
141
|
|
|
139
142
|
payload_base64 = id_token.split('.')[1]
|
|
140
143
|
payload = _decode_base64(payload_base64)
|
|
141
144
|
payload_json = json.loads(payload)
|
|
142
145
|
|
|
143
|
-
_validate_header(header_json,
|
|
146
|
+
_validate_header(header_json, allowed_algs)
|
|
147
|
+
_validate_signature(issuer, client_id, allowed_algs, id_token, jwks_response_body)
|
|
144
148
|
_validate_payload(payload_json, nonce, callback_body, client_id)
|
|
145
149
|
|
|
146
150
|
return header_json, payload_json
|
|
@@ -193,6 +197,8 @@ def _validate_payload(json_payload: dict[str, Any], nonce: str, callback_body: C
|
|
|
193
197
|
if json_payload['aud'] != client_id:
|
|
194
198
|
raise RelyingPartyClientSdkException('aud claim in token response does not match client ID')
|
|
195
199
|
|
|
200
|
+
def _get_unverified_header(id_token:str) -> Any:
|
|
201
|
+
return extract_header(id_token.split('.')[0].encode(), DecodeError)
|
|
196
202
|
|
|
197
203
|
def _decode_base64(base64_str: str) -> str:
|
|
198
204
|
"""
|
|
@@ -204,51 +210,61 @@ def _decode_base64(base64_str: str) -> str:
|
|
|
204
210
|
return decoded_bytes.decode('utf-8')
|
|
205
211
|
|
|
206
212
|
|
|
207
|
-
def _validate_header(json_header
|
|
213
|
+
def _validate_header(json_header: dict, allowed_algs: list) -> None:
|
|
208
214
|
if 'alg' not in json_header or json_header['alg'] == 'none':
|
|
209
215
|
raise RelyingPartyClientSdkException("no alg value in the token response jwt header")
|
|
210
216
|
|
|
211
217
|
if 'kid' not in json_header:
|
|
212
218
|
raise RelyingPartyClientSdkException("no kid value in the token response jwt header")
|
|
213
219
|
|
|
214
|
-
kid = json_header['kid']
|
|
215
|
-
jwk = None
|
|
216
|
-
|
|
217
|
-
for key_element in jwks.get('keys', []):
|
|
218
|
-
if _matching_kid(kid, key_element) and _none_or_matching_alg(json_header, key_element):
|
|
219
|
-
jwk = key_element
|
|
220
|
-
break
|
|
221
|
-
|
|
222
|
-
if jwk is None:
|
|
223
|
-
raise RelyingPartyClientSdkException(
|
|
224
|
-
f"kid provided does not match any key in the jwks for specified algorithm in "
|
|
225
|
-
f"token header: {json_header['alg']}")
|
|
226
|
-
|
|
227
220
|
found_alg = any(alg.lower() == json_header['alg'].lower() for alg in allowed_algs)
|
|
228
221
|
if not found_alg:
|
|
229
222
|
raise RelyingPartyClientSdkException(
|
|
230
|
-
"alg value in id token header is not one of the supported algorithms listed on the well-known"
|
|
231
|
-
|
|
232
|
-
# try:
|
|
233
|
-
# validate_signature(token_input.get('id_token'), jwk)
|
|
234
|
-
# except (JOSEError, ValueError) as e:
|
|
235
|
-
# raise RelyingPartyClientSdkException(f"exception while validation jwt signature: {e}")
|
|
223
|
+
f"alg value in id token header is not one of the supported algorithms listed on the well-known. "
|
|
224
|
+
f"Provided alg {json_header['alg']}")
|
|
236
225
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def
|
|
244
|
-
|
|
226
|
+
def _find_key_in_jwks(kid: str, token_alg: str, jwks: dict) -> Any:
|
|
227
|
+
for key_element in jwks.get('keys', []):
|
|
228
|
+
if _matching_kid(kid, key_element) and _matching_alg(token_alg, key_element) and _matching_use(key_element):
|
|
229
|
+
return key_element
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
def _validate_signature(issuer: str, audience: str, allowed_alg: list, id_token: str, jwks: dict) -> None:
|
|
233
|
+
try:
|
|
234
|
+
# Pub key extraction
|
|
235
|
+
header = _get_unverified_header(id_token)
|
|
236
|
+
kid = header['kid']
|
|
237
|
+
alg = header['alg']
|
|
238
|
+
jwk = _find_key_in_jwks(kid, alg, jwks)
|
|
239
|
+
|
|
240
|
+
if jwk is None:
|
|
241
|
+
raise RelyingPartyClientSdkException(
|
|
242
|
+
f"kid and alg provided does not match any key in the jwks for specified algorithm in "
|
|
243
|
+
f"token header, kid: {kid}, alg: {alg}")
|
|
244
|
+
|
|
245
|
+
# Decode JWT with signature verification
|
|
246
|
+
claims_options = {
|
|
247
|
+
"iss": {"essential": True, "value": issuer},
|
|
248
|
+
"aud": {"essential": True, "value": audience}
|
|
249
|
+
}
|
|
250
|
+
jwt = JsonWebToken(allowed_alg)
|
|
251
|
+
claims = jwt.decode(id_token, key=jwk, claims_options=claims_options)
|
|
252
|
+
claims.validate()
|
|
253
|
+
logger.info("JWT signature is valid")
|
|
254
|
+
except BadSignatureError as e:
|
|
255
|
+
raise RelyingPartyClientSdkException(f"Invalid JWT signature: {e}, token data: {e.result}")
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.debug(f"Invalid JWT token: {id_token}")
|
|
258
|
+
raise RelyingPartyClientSdkException(f"Invalid JWT signature: {e}")
|
|
259
|
+
|
|
260
|
+
def _matching_use(key_element: dict) -> bool:
|
|
261
|
+
return key_element.get('use') == "sig"
|
|
262
|
+
|
|
263
|
+
def _matching_kid(kid: str, key_element: dict) -> bool:
|
|
245
264
|
return key_element.get('kid') == kid
|
|
246
265
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# Implement this to check if the 'alg' in JWT matches the one in the key element, if required
|
|
250
|
-
return True
|
|
251
|
-
|
|
266
|
+
def _matching_alg(token_alg: str, key_element: dict) -> bool:
|
|
267
|
+
return key_element.get("alg") is None or key_element.get("alg") == token_alg
|
|
252
268
|
|
|
253
269
|
def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
|
|
254
270
|
headers = {
|
|
@@ -263,4 +279,4 @@ def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str)
|
|
|
263
279
|
f"Response code for JWKS to {issuer_metadata.jwks_uri} not successful, was: {response.status_code}, "
|
|
264
280
|
f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
|
|
265
281
|
|
|
266
|
-
return response.json()
|
|
282
|
+
return response.json()
|
|
@@ -32,7 +32,7 @@ def from_dict(data_class: Type[T], data: Union[dict, list], pascal_case=False) -
|
|
|
32
32
|
for field, field_type in fieldtypes.items():
|
|
33
33
|
# Convert the snake_case field name to PascalCase to match the JSON key
|
|
34
34
|
json_field = field
|
|
35
|
-
if
|
|
35
|
+
if pascal_case:
|
|
36
36
|
json_field = snake_to_pascal(field)
|
|
37
37
|
|
|
38
38
|
# Fetch the corresponding value from the JSON data
|
|
File without changes
|
|
File without changes
|
{rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rp_python_sdk-0.9.0 → rp_python_sdk-0.11.0}/rp_python_sdk/relying_party_client_sdk_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|