rp_python_sdk 0.9.0__tar.gz → 0.10.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.10.0}/PKG-INFO +4 -1
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/README.md +3 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/pyproject.toml +2 -2
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/retrieve_tokens.py +55 -40
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/util/mapping.py +1 -1
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/__init__.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/get_participants.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/user_info.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/filters/participant_filters.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/model.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/relying_party_client_sdk.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.0}/rp_python_sdk/sdk_config.py +0 -0
- {rp_python_sdk-0.9.0 → rp_python_sdk-0.10.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.10.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,9 @@ poetry publish
|
|
|
426
426
|
|
|
427
427
|
## Release Notes
|
|
428
428
|
|
|
429
|
+
### 0.10.0 (Oct 24, 2024)
|
|
430
|
+
- JWT signature verification for id_token in the retrieve_tokens step.
|
|
431
|
+
|
|
429
432
|
### 0.9.0 (Oct 18, 2024)
|
|
430
433
|
- Added support for Python 3.13
|
|
431
434
|
- Added support for `payment` ApiResources
|
|
@@ -407,6 +407,9 @@ poetry publish
|
|
|
407
407
|
|
|
408
408
|
## Release Notes
|
|
409
409
|
|
|
410
|
+
### 0.10.0 (Oct 24, 2024)
|
|
411
|
+
- JWT signature verification for id_token in the retrieve_tokens step.
|
|
412
|
+
|
|
410
413
|
### 0.9.0 (Oct 18, 2024)
|
|
411
414
|
- Added support for Python 3.13
|
|
412
415
|
- 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.10.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
|
|
@@ -103,7 +106,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
103
106
|
jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
|
|
104
107
|
allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
|
|
105
108
|
|
|
106
|
-
header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body)
|
|
109
|
+
header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body, issuer_metadata.issuer)
|
|
107
110
|
|
|
108
111
|
token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
|
|
109
112
|
|
|
@@ -119,7 +122,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
119
122
|
|
|
120
123
|
def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allowed_algs: list[str],
|
|
121
124
|
nonce: str, client_id: str,
|
|
122
|
-
callback_body: CallbackBody) -> Tuple[dict[str, Any], dict[str, Any]]:
|
|
125
|
+
callback_body: CallbackBody, issuer: str) -> Tuple[dict[str, Any], dict[str, Any]]:
|
|
123
126
|
"""
|
|
124
127
|
Decode and parse an ID token into JSON objects for header and payload.
|
|
125
128
|
|
|
@@ -129,18 +132,18 @@ def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allow
|
|
|
129
132
|
:param nonce
|
|
130
133
|
:param client_id
|
|
131
134
|
:param callback_body
|
|
135
|
+
:param issuer
|
|
132
136
|
:return: two dictionaries for header and payload
|
|
133
137
|
"""
|
|
134
138
|
id_token = token_input.id_token
|
|
135
|
-
|
|
136
|
-
header = _decode_base64(header_base64)
|
|
137
|
-
header_json = json.loads(header)
|
|
139
|
+
header_json = _get_unverified_header(id_token)
|
|
138
140
|
|
|
139
141
|
payload_base64 = id_token.split('.')[1]
|
|
140
142
|
payload = _decode_base64(payload_base64)
|
|
141
143
|
payload_json = json.loads(payload)
|
|
142
144
|
|
|
143
|
-
_validate_header(header_json,
|
|
145
|
+
_validate_header(header_json, allowed_algs)
|
|
146
|
+
_validate_signature(issuer, client_id, allowed_algs, id_token, jwks_response_body)
|
|
144
147
|
_validate_payload(payload_json, nonce, callback_body, client_id)
|
|
145
148
|
|
|
146
149
|
return header_json, payload_json
|
|
@@ -193,6 +196,8 @@ def _validate_payload(json_payload: dict[str, Any], nonce: str, callback_body: C
|
|
|
193
196
|
if json_payload['aud'] != client_id:
|
|
194
197
|
raise RelyingPartyClientSdkException('aud claim in token response does not match client ID')
|
|
195
198
|
|
|
199
|
+
def _get_unverified_header(id_token:str) -> Any:
|
|
200
|
+
return extract_header(id_token.split('.')[0].encode(), DecodeError)
|
|
196
201
|
|
|
197
202
|
def _decode_base64(base64_str: str) -> str:
|
|
198
203
|
"""
|
|
@@ -204,51 +209,61 @@ def _decode_base64(base64_str: str) -> str:
|
|
|
204
209
|
return decoded_bytes.decode('utf-8')
|
|
205
210
|
|
|
206
211
|
|
|
207
|
-
def _validate_header(json_header
|
|
212
|
+
def _validate_header(json_header: dict, allowed_algs: list) -> None:
|
|
208
213
|
if 'alg' not in json_header or json_header['alg'] == 'none':
|
|
209
214
|
raise RelyingPartyClientSdkException("no alg value in the token response jwt header")
|
|
210
215
|
|
|
211
216
|
if 'kid' not in json_header:
|
|
212
217
|
raise RelyingPartyClientSdkException("no kid value in the token response jwt header")
|
|
213
218
|
|
|
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
219
|
found_alg = any(alg.lower() == json_header['alg'].lower() for alg in allowed_algs)
|
|
228
220
|
if not found_alg:
|
|
229
221
|
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}")
|
|
222
|
+
f"alg value in id token header is not one of the supported algorithms listed on the well-known. "
|
|
223
|
+
f"Provided alg {json_header['alg']}")
|
|
236
224
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def
|
|
244
|
-
|
|
225
|
+
def _find_key_in_jwks(kid: str, token_alg: str, jwks: dict) -> Any:
|
|
226
|
+
for key_element in jwks.get('keys', []):
|
|
227
|
+
if _matching_kid(kid, key_element) and _matching_alg(token_alg, key_element) and _matching_use(key_element):
|
|
228
|
+
return key_element
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def _validate_signature(issuer: str, audience: str, allowed_alg: list, id_token: str, jwks: dict) -> None:
|
|
232
|
+
try:
|
|
233
|
+
# Pub key extraction
|
|
234
|
+
header = _get_unverified_header(id_token)
|
|
235
|
+
kid = header['kid']
|
|
236
|
+
alg = header['alg']
|
|
237
|
+
jwk = _find_key_in_jwks(kid, alg, jwks)
|
|
238
|
+
|
|
239
|
+
if jwk is None:
|
|
240
|
+
raise RelyingPartyClientSdkException(
|
|
241
|
+
f"kid and alg provided does not match any key in the jwks for specified algorithm in "
|
|
242
|
+
f"token header, kid: {kid}, alg: {alg}")
|
|
243
|
+
|
|
244
|
+
# Decode JWT with signature verification
|
|
245
|
+
claims_options = {
|
|
246
|
+
"iss": {"essential": True, "value": issuer},
|
|
247
|
+
"aud": {"essential": True, "value": audience}
|
|
248
|
+
}
|
|
249
|
+
jwt = JsonWebToken(allowed_alg)
|
|
250
|
+
claims = jwt.decode(id_token, key=jwk, claims_options=claims_options)
|
|
251
|
+
claims.validate()
|
|
252
|
+
logger.info("JWT signature is valid")
|
|
253
|
+
except BadSignatureError as e:
|
|
254
|
+
raise RelyingPartyClientSdkException(f"Invalid JWT signature: {e}, token data: {e.result}")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.debug(f"Invalid JWT token: {id_token}")
|
|
257
|
+
raise RelyingPartyClientSdkException(f"Invalid JWT signature: {e}")
|
|
258
|
+
|
|
259
|
+
def _matching_use(key_element: dict) -> bool:
|
|
260
|
+
return key_element.get('use') == "sig"
|
|
261
|
+
|
|
262
|
+
def _matching_kid(kid: str, key_element: dict) -> bool:
|
|
245
263
|
return key_element.get('kid') == kid
|
|
246
264
|
|
|
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
|
-
|
|
265
|
+
def _matching_alg(token_alg: str, key_element: dict) -> bool:
|
|
266
|
+
return key_element.get("alg") is None or key_element.get("alg") == token_alg
|
|
252
267
|
|
|
253
268
|
def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
|
|
254
269
|
headers = {
|
|
@@ -263,4 +278,4 @@ def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str)
|
|
|
263
278
|
f"Response code for JWKS to {issuer_metadata.jwks_uri} not successful, was: {response.status_code}, "
|
|
264
279
|
f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
|
|
265
280
|
|
|
266
|
-
return response.json()
|
|
281
|
+
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.10.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.10.0}/rp_python_sdk/relying_party_client_sdk_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|