rp_python_sdk 0.7.1__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.7.1/README.md → rp_python_sdk-0.10.0/PKG-INFO +44 -1
- rp_python_sdk-0.7.1/PKG-INFO → rp_python_sdk-0.10.0/README.md +23 -16
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/pyproject.toml +12 -9
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/get_participants.py +6 -3
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py +6 -9
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/retrieve_tokens.py +66 -47
- rp_python_sdk-0.10.0/rp_python_sdk/endpoints/util/mapping.py +54 -0
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/filters/participant_filters.py +0 -1
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/model.py +32 -17
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/relying_party_client_sdk.py +5 -1
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/__init__.py +0 -0
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/user_info.py +0 -0
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/util/fapi.py +0 -0
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/relying_party_client_sdk_exception.py +0 -0
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/sdk_config.py +0 -0
- {rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/setup_logger.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: rp_python_sdk
|
|
3
|
+
Version: 0.10.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
|
+
Requires-Dist: authlib (>=1.3.2,<2.0.0)
|
|
14
|
+
Requires-Dist: cffi (>=1.17.1,<2.0.0)
|
|
15
|
+
Requires-Dist: cryptography (>=43.0.1,<44.0.0)
|
|
16
|
+
Requires-Dist: joserfc (>=1.0.0,<2.0.0)
|
|
17
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
1
20
|
# Python SDK for Relying Parties
|
|
2
21
|
|
|
3
22
|
[](https://github.com/connectid-tools/rp-python-sdk/actions/workflows/build.yml)
|
|
@@ -407,6 +426,29 @@ poetry publish
|
|
|
407
426
|
|
|
408
427
|
## Release Notes
|
|
409
428
|
|
|
429
|
+
### 0.10.0 (Oct 24, 2024)
|
|
430
|
+
- JWT signature verification for id_token in the retrieve_tokens step.
|
|
431
|
+
|
|
432
|
+
### 0.9.0 (Oct 18, 2024)
|
|
433
|
+
- Added support for Python 3.13
|
|
434
|
+
- Added support for `payment` ApiResources
|
|
435
|
+
- Updated all dependencies
|
|
436
|
+
|
|
437
|
+
### 0.8.0 (Sep 3, 2024)
|
|
438
|
+
- Breaking change: When `essential_claims` was empty it used the `default_claims` list. That logic has been removed.
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
def send_pushed_authorisation_request(config: SdkConfig,
|
|
442
|
+
authorisation_server_id: str,
|
|
443
|
+
essential_claims: set[str],
|
|
444
|
+
voluntary_claims: set[str],
|
|
445
|
+
purpose: str) -> PARResponse:
|
|
446
|
+
# this has been removed
|
|
447
|
+
if len(essential_claims) == 0:
|
|
448
|
+
essential_claims = default_claims
|
|
449
|
+
...
|
|
450
|
+
```
|
|
451
|
+
|
|
410
452
|
### 0.7.1 (June 28, 2024)
|
|
411
453
|
- Made `scope` field optional in accordance with rfc-6749.
|
|
412
454
|
|
|
@@ -429,4 +471,5 @@ poetry publish
|
|
|
429
471
|
- Implemented all endpoints
|
|
430
472
|
|
|
431
473
|
### 0.1.0 (March 27, 2024)
|
|
432
|
-
- Initial version
|
|
474
|
+
- Initial version
|
|
475
|
+
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: rp_python_sdk
|
|
3
|
-
Version: 0.7.1
|
|
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
|
-
Requires-Dist: authlib (>=1.3.0,<2.0.0)
|
|
13
|
-
Requires-Dist: dataclass-wizard (>=0.22.3,<0.23.0)
|
|
14
|
-
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
|
|
17
1
|
# Python SDK for Relying Parties
|
|
18
2
|
|
|
19
3
|
[](https://github.com/connectid-tools/rp-python-sdk/actions/workflows/build.yml)
|
|
@@ -423,6 +407,29 @@ poetry publish
|
|
|
423
407
|
|
|
424
408
|
## Release Notes
|
|
425
409
|
|
|
410
|
+
### 0.10.0 (Oct 24, 2024)
|
|
411
|
+
- JWT signature verification for id_token in the retrieve_tokens step.
|
|
412
|
+
|
|
413
|
+
### 0.9.0 (Oct 18, 2024)
|
|
414
|
+
- Added support for Python 3.13
|
|
415
|
+
- Added support for `payment` ApiResources
|
|
416
|
+
- Updated all dependencies
|
|
417
|
+
|
|
418
|
+
### 0.8.0 (Sep 3, 2024)
|
|
419
|
+
- Breaking change: When `essential_claims` was empty it used the `default_claims` list. That logic has been removed.
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
def send_pushed_authorisation_request(config: SdkConfig,
|
|
423
|
+
authorisation_server_id: str,
|
|
424
|
+
essential_claims: set[str],
|
|
425
|
+
voluntary_claims: set[str],
|
|
426
|
+
purpose: str) -> PARResponse:
|
|
427
|
+
# this has been removed
|
|
428
|
+
if len(essential_claims) == 0:
|
|
429
|
+
essential_claims = default_claims
|
|
430
|
+
...
|
|
431
|
+
```
|
|
432
|
+
|
|
426
433
|
### 0.7.1 (June 28, 2024)
|
|
427
434
|
- Made `scope` field optional in accordance with rfc-6749.
|
|
428
435
|
|
|
@@ -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"
|
|
@@ -8,20 +8,23 @@ readme = "README.md"
|
|
|
8
8
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
10
|
python = "^3.12"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
requests = "^2.32.3"
|
|
12
|
+
authlib = "^1.3.2"
|
|
13
|
+
cryptography = "^43.0.1"
|
|
14
|
+
joserfc = "^1.0.0"
|
|
15
|
+
cffi = "^1.17.1"
|
|
14
16
|
|
|
15
17
|
[tool.poetry.group.test.dependencies]
|
|
16
|
-
pytest = "^8.
|
|
17
|
-
httpx = "^0.27.
|
|
18
|
+
pytest = "^8.3.3"
|
|
19
|
+
httpx = "^0.27.2"
|
|
18
20
|
|
|
19
21
|
[tool.poetry.group.dev.dependencies]
|
|
20
|
-
black = "^24.
|
|
21
|
-
pre-commit = "^3.
|
|
22
|
-
mypy = "^1.
|
|
22
|
+
black = "^24.10.0"
|
|
23
|
+
pre-commit = "^3.8.0"
|
|
24
|
+
mypy = "^1.13.0"
|
|
23
25
|
requests-mock = "^1.12.1"
|
|
24
26
|
freezegun = "^1.4.0"
|
|
27
|
+
flake8 = "^7.1.1"
|
|
25
28
|
|
|
26
29
|
[build-system]
|
|
27
30
|
requires = ["poetry-core"]
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from datetime import date
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
|
+
import json
|
|
4
5
|
|
|
6
|
+
from rp_python_sdk.endpoints.util.mapping import from_dict
|
|
5
7
|
from rp_python_sdk.filters.participant_filters import remove_out_of_date_certifications, \
|
|
6
8
|
remove_unofficial_certifications, filter_for_fallback_identity_service_providers, \
|
|
7
9
|
remove_participants_without_auth_servers, remove_fallback_identity_service_provider, \
|
|
@@ -65,7 +67,8 @@ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
|
|
|
65
67
|
raise RelyingPartyClientSdkException(
|
|
66
68
|
f"Call to identity provider endpoint failed with code: {response.status_code}")
|
|
67
69
|
|
|
68
|
-
participant_list: list[Participant] = Participant.from_json(response.text)
|
|
70
|
+
# participant_list: list[Participant] = Participant.from_json(response.text)
|
|
71
|
+
participant_list: list[Participant] = from_dict(Participant, json.loads(response.text), pascal_case=True)
|
|
69
72
|
|
|
70
73
|
logger.info(f"Retrieved identity providers, num orgs found: {len(participant_list)}")
|
|
71
74
|
return participant_list
|
|
@@ -83,14 +86,14 @@ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) ->
|
|
|
83
86
|
|
|
84
87
|
|
|
85
88
|
def get_issuer_metadata(authorisation_server: AuthorisationServer):
|
|
86
|
-
auth_server_issuer = authorisation_server.
|
|
89
|
+
auth_server_issuer = authorisation_server.open_i_d_discovery_document
|
|
87
90
|
|
|
88
91
|
try:
|
|
89
92
|
logger.info(f"Getting discovery document from {auth_server_issuer}")
|
|
90
93
|
response = requests.get(auth_server_issuer)
|
|
91
94
|
response.raise_for_status() # Raises an exception for 4XX or 5XX errors
|
|
92
95
|
|
|
93
|
-
return IssuerMetadata.
|
|
96
|
+
return from_dict(IssuerMetadata, json.loads(response.text))
|
|
94
97
|
|
|
95
98
|
except requests.exceptions.HTTPError as http_err:
|
|
96
99
|
raise RelyingPartyClientSdkException(
|
{rp_python_sdk-0.7.1 → rp_python_sdk-0.10.0}/rp_python_sdk/endpoints/pushed_authorisation_request.py
RENAMED
|
@@ -16,9 +16,6 @@ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientS
|
|
|
16
16
|
from rp_python_sdk.sdk_config import SdkConfig
|
|
17
17
|
from rp_python_sdk.setup_logger import logger
|
|
18
18
|
|
|
19
|
-
default_claims = {"name", "given_name", "middle_name", "family_name", "phone_number", "email", "address", "birthdate",
|
|
20
|
-
"txn"}
|
|
21
|
-
|
|
22
19
|
extended_claims = {"over16", "over18", "over21", "over25", "over65", "beneficiary_account_au",
|
|
23
20
|
"beneficiary_account_au_payid", "beneficiary_account_international"}
|
|
24
21
|
|
|
@@ -28,9 +25,6 @@ def send_pushed_authorisation_request(config: SdkConfig,
|
|
|
28
25
|
essential_claims: set[str],
|
|
29
26
|
voluntary_claims: set[str],
|
|
30
27
|
purpose: str) -> PARResponse:
|
|
31
|
-
if len(essential_claims) == 0:
|
|
32
|
-
essential_claims = default_claims
|
|
33
|
-
|
|
34
28
|
_check_valid_purpose(purpose)
|
|
35
29
|
|
|
36
30
|
checked_essential_claims = _ensure_mandatory_claims_present(essential_claims, voluntary_claims)
|
|
@@ -117,10 +111,12 @@ def send_pushed_authorisation_request(config: SdkConfig,
|
|
|
117
111
|
cert=(config.transport_pem, config.transport_key),
|
|
118
112
|
verify=config.ca_pem)
|
|
119
113
|
|
|
120
|
-
log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request",
|
|
114
|
+
log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request",
|
|
115
|
+
x_fapi_interaction_id, response)
|
|
121
116
|
|
|
122
117
|
if not response.ok:
|
|
123
|
-
raise RelyingPartyClientSdkException(f"Response code for PAR to {authorisation_server_id} not successful,
|
|
118
|
+
raise RelyingPartyClientSdkException(f"Response code for PAR to {authorisation_server_id} not successful, "
|
|
119
|
+
f"was: {response.status_code} x-fapi-interaction-id: {x_fapi_interaction_id}")
|
|
124
120
|
|
|
125
121
|
request_uri = response.json()['request_uri']
|
|
126
122
|
logger.debug(f"PAR was sent, request_uri='{request_uri}'")
|
|
@@ -138,7 +134,8 @@ def _check_valid_purpose(purpose) -> None:
|
|
|
138
134
|
contains_illegal_char = any(char in purpose for char in illegal_chars)
|
|
139
135
|
if contains_illegal_char:
|
|
140
136
|
raise RelyingPartyClientSdkException(
|
|
141
|
-
f"Purpose cannot contain any of the following characters: {', '.join(illegal_chars)},
|
|
137
|
+
f"Purpose cannot contain any of the following characters: {', '.join(illegal_chars)}, "
|
|
138
|
+
f"purpose supplied: [{purpose}]")
|
|
142
139
|
|
|
143
140
|
|
|
144
141
|
def _ensure_mandatory_claims_present(essential_claims: set[str], voluntary_claims: set[str]) -> set[str]:
|
|
@@ -5,12 +5,18 @@ import uuid
|
|
|
5
5
|
from typing import Tuple, Any
|
|
6
6
|
|
|
7
7
|
import requests
|
|
8
|
-
from
|
|
8
|
+
from joserfc import jws
|
|
9
|
+
from joserfc.jwk import RSAKey
|
|
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
|
|
9
14
|
|
|
10
15
|
from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
|
|
11
16
|
get_auth_server_details
|
|
12
17
|
from rp_python_sdk.endpoints.user_info import call_user_info
|
|
13
18
|
from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id, log_x_fapi_interaction_id_matches
|
|
19
|
+
from rp_python_sdk.endpoints.util.mapping import from_dict
|
|
14
20
|
from rp_python_sdk.model import CallbackBody, TokenSet, TokenInput, IssuerMetadata
|
|
15
21
|
from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
|
|
16
22
|
from rp_python_sdk.sdk_config import SdkConfig
|
|
@@ -60,18 +66,19 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
60
66
|
'iat': iat,
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
# See https://jose.authlib.org/en/guide/registry/
|
|
70
|
+
registry = JWSRegistry(algorithms=["PS256"])
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
key = RSAKey.import_key(config.signing_key)
|
|
73
|
+
header = {'alg': 'PS256', 'kid': config.signing_kid}
|
|
74
|
+
client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload), key, registry = registry)
|
|
68
75
|
|
|
69
76
|
# Data payload
|
|
70
77
|
data = {
|
|
71
78
|
"grant_type": "authorization_code",
|
|
72
79
|
"code": callback_body.code,
|
|
73
80
|
"redirect_uri": redirect_uri,
|
|
74
|
-
"client_assertion": client_assertion_jwt
|
|
81
|
+
"client_assertion": client_assertion_jwt,
|
|
75
82
|
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
76
83
|
"code_verifier": original_code_verifier
|
|
77
84
|
}
|
|
@@ -94,12 +101,12 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
94
101
|
f"Response code for token endpoint to {token_endpoint} not successful, was: {response.status_code}, "
|
|
95
102
|
f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: " + response.text)
|
|
96
103
|
|
|
97
|
-
token_input = TokenInput.
|
|
104
|
+
token_input = from_dict(TokenInput, json.loads(response.text))
|
|
98
105
|
|
|
99
106
|
jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
|
|
100
107
|
allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
|
|
101
108
|
|
|
102
|
-
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)
|
|
103
110
|
|
|
104
111
|
token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
|
|
105
112
|
|
|
@@ -115,7 +122,7 @@ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_bo
|
|
|
115
122
|
|
|
116
123
|
def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allowed_algs: list[str],
|
|
117
124
|
nonce: str, client_id: str,
|
|
118
|
-
callback_body: CallbackBody) -> Tuple[dict[str, Any], dict[str, Any]]:
|
|
125
|
+
callback_body: CallbackBody, issuer: str) -> Tuple[dict[str, Any], dict[str, Any]]:
|
|
119
126
|
"""
|
|
120
127
|
Decode and parse an ID token into JSON objects for header and payload.
|
|
121
128
|
|
|
@@ -125,18 +132,18 @@ def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allow
|
|
|
125
132
|
:param nonce
|
|
126
133
|
:param client_id
|
|
127
134
|
:param callback_body
|
|
135
|
+
:param issuer
|
|
128
136
|
:return: two dictionaries for header and payload
|
|
129
137
|
"""
|
|
130
138
|
id_token = token_input.id_token
|
|
131
|
-
|
|
132
|
-
header = _decode_base64(header_base64)
|
|
133
|
-
header_json = json.loads(header)
|
|
139
|
+
header_json = _get_unverified_header(id_token)
|
|
134
140
|
|
|
135
141
|
payload_base64 = id_token.split('.')[1]
|
|
136
142
|
payload = _decode_base64(payload_base64)
|
|
137
143
|
payload_json = json.loads(payload)
|
|
138
144
|
|
|
139
|
-
_validate_header(header_json,
|
|
145
|
+
_validate_header(header_json, allowed_algs)
|
|
146
|
+
_validate_signature(issuer, client_id, allowed_algs, id_token, jwks_response_body)
|
|
140
147
|
_validate_payload(payload_json, nonce, callback_body, client_id)
|
|
141
148
|
|
|
142
149
|
return header_json, payload_json
|
|
@@ -189,6 +196,8 @@ def _validate_payload(json_payload: dict[str, Any], nonce: str, callback_body: C
|
|
|
189
196
|
if json_payload['aud'] != client_id:
|
|
190
197
|
raise RelyingPartyClientSdkException('aud claim in token response does not match client ID')
|
|
191
198
|
|
|
199
|
+
def _get_unverified_header(id_token:str) -> Any:
|
|
200
|
+
return extract_header(id_token.split('.')[0].encode(), DecodeError)
|
|
192
201
|
|
|
193
202
|
def _decode_base64(base64_str: str) -> str:
|
|
194
203
|
"""
|
|
@@ -200,51 +209,61 @@ def _decode_base64(base64_str: str) -> str:
|
|
|
200
209
|
return decoded_bytes.decode('utf-8')
|
|
201
210
|
|
|
202
211
|
|
|
203
|
-
def _validate_header(json_header
|
|
212
|
+
def _validate_header(json_header: dict, allowed_algs: list) -> None:
|
|
204
213
|
if 'alg' not in json_header or json_header['alg'] == 'none':
|
|
205
214
|
raise RelyingPartyClientSdkException("no alg value in the token response jwt header")
|
|
206
215
|
|
|
207
216
|
if 'kid' not in json_header:
|
|
208
217
|
raise RelyingPartyClientSdkException("no kid value in the token response jwt header")
|
|
209
218
|
|
|
210
|
-
kid = json_header['kid']
|
|
211
|
-
jwk = None
|
|
212
|
-
|
|
213
|
-
for key_element in jwks.get('keys', []):
|
|
214
|
-
if _matching_kid(kid, key_element) and _none_or_matching_alg(json_header, key_element):
|
|
215
|
-
jwk = key_element
|
|
216
|
-
break
|
|
217
|
-
|
|
218
|
-
if jwk is None:
|
|
219
|
-
raise RelyingPartyClientSdkException(
|
|
220
|
-
f"kid provided does not match any key in the jwks for specified algorithm in "
|
|
221
|
-
f"token header: {json_header['alg']}")
|
|
222
|
-
|
|
223
219
|
found_alg = any(alg.lower() == json_header['alg'].lower() for alg in allowed_algs)
|
|
224
220
|
if not found_alg:
|
|
225
221
|
raise RelyingPartyClientSdkException(
|
|
226
|
-
"alg value in id token header is not one of the supported algorithms listed on the well-known"
|
|
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']}")
|
|
227
224
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _validate_signature(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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:
|
|
241
263
|
return key_element.get('kid') == kid
|
|
242
264
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# Implement this to check if the 'alg' in JWT matches the one in the key element, if required
|
|
246
|
-
return True
|
|
247
|
-
|
|
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
|
|
248
267
|
|
|
249
268
|
def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
|
|
250
269
|
headers = {
|
|
@@ -259,4 +278,4 @@ def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str)
|
|
|
259
278
|
f"Response code for JWKS to {issuer_metadata.jwks_uri} not successful, was: {response.status_code}, "
|
|
260
279
|
f"x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
|
|
261
280
|
|
|
262
|
-
return response.json()
|
|
281
|
+
return response.json()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Type, TypeVar, Union, List, get_type_hints
|
|
2
|
+
|
|
3
|
+
T = TypeVar('T')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def snake_to_pascal(name: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Converts a snake_case string to PascalCase.
|
|
9
|
+
Example: organisation_id -> OrganisationId
|
|
10
|
+
"""
|
|
11
|
+
return ''.join(word.capitalize() for word in name.split('_'))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def from_dict(data_class: Type[T], data: Union[dict, list], pascal_case=False) -> Union[T, List[T]]:
|
|
15
|
+
"""
|
|
16
|
+
Recursively maps a dictionary or a list of dictionaries to a dataclass or a list of dataclasses.
|
|
17
|
+
Handles key transformations from snake_case to PascalCase, and resolves forward references.
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(data, list):
|
|
20
|
+
# Map each item in the list to the dataclass
|
|
21
|
+
return [from_dict(data_class, item, pascal_case) for item in data]
|
|
22
|
+
|
|
23
|
+
if not isinstance(data, dict):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"Expected a dictionary to map to {data_class.__name__}, but got {type(data).__name__}: {data}")
|
|
26
|
+
|
|
27
|
+
# Get the fields of the dataclass, resolving forward references
|
|
28
|
+
fieldtypes = get_type_hints(data_class)
|
|
29
|
+
|
|
30
|
+
# Prepare the arguments for the dataclass constructor
|
|
31
|
+
data_class_kwargs = {}
|
|
32
|
+
for field, field_type in fieldtypes.items():
|
|
33
|
+
# Convert the snake_case field name to PascalCase to match the JSON key
|
|
34
|
+
json_field = field
|
|
35
|
+
if pascal_case:
|
|
36
|
+
json_field = snake_to_pascal(field)
|
|
37
|
+
|
|
38
|
+
# Fetch the corresponding value from the JSON data
|
|
39
|
+
field_value = data.get(json_field)
|
|
40
|
+
|
|
41
|
+
if isinstance(field_value, list):
|
|
42
|
+
inner_type = field_type.__args__[0]
|
|
43
|
+
data_class_kwargs[field] = [from_dict(inner_type, item, pascal_case) if isinstance(item, dict) else item for
|
|
44
|
+
item in field_value]
|
|
45
|
+
elif isinstance(field_value, dict):
|
|
46
|
+
data_class_kwargs[field] = from_dict(field_type, field_value, pascal_case)
|
|
47
|
+
else:
|
|
48
|
+
# Handle authorisation_servers case
|
|
49
|
+
if field == "authorisation_servers" and field_value is None:
|
|
50
|
+
data_class_kwargs[field] = []
|
|
51
|
+
else:
|
|
52
|
+
data_class_kwargs[field] = field_value
|
|
53
|
+
|
|
54
|
+
return data_class(**data_class_kwargs)
|
|
@@ -2,11 +2,8 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import Any, Dict, Optional
|
|
3
3
|
from typing import List
|
|
4
4
|
|
|
5
|
-
from dataclass_wizard import JSONWizard
|
|
6
|
-
|
|
7
|
-
|
|
8
5
|
@dataclass
|
|
9
|
-
class Participant
|
|
6
|
+
class Participant:
|
|
10
7
|
organisation_id: str
|
|
11
8
|
status: str
|
|
12
9
|
organisation_name: str
|
|
@@ -29,7 +26,7 @@ class Participant(JSONWizard):
|
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
@dataclass
|
|
32
|
-
class AuthorisationServerCertification
|
|
29
|
+
class AuthorisationServerCertification:
|
|
33
30
|
certification_start_date: str # date
|
|
34
31
|
certification_expiration_date: str # date
|
|
35
32
|
certification_id: str
|
|
@@ -42,31 +39,49 @@ class AuthorisationServerCertification(JSONWizard):
|
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
@dataclass
|
|
45
|
-
class OrgDomainClaim
|
|
42
|
+
class OrgDomainClaim:
|
|
46
43
|
authorisation_domain_name: str
|
|
47
44
|
authority_name: str
|
|
48
45
|
registration_id: str
|
|
49
46
|
status: str
|
|
50
47
|
|
|
48
|
+
@dataclass
|
|
49
|
+
class ApiDiscoveryEndpoint:
|
|
50
|
+
api_discovery_id: str
|
|
51
|
+
api_endpoint: str
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ApiResource:
|
|
55
|
+
api_resource_id: str
|
|
56
|
+
api_version: str
|
|
57
|
+
api_discovery_endpoints: List[ApiDiscoveryEndpoint]
|
|
58
|
+
family_complete: bool
|
|
59
|
+
api_certification_uri: Optional[str]
|
|
60
|
+
certification_status: Optional[str]
|
|
61
|
+
# should be a date
|
|
62
|
+
certification_start_date: str
|
|
63
|
+
# should be a date
|
|
64
|
+
certification_expiration_date: str
|
|
65
|
+
api_family_type: Optional[str]
|
|
51
66
|
|
|
52
67
|
@dataclass
|
|
53
|
-
class AuthorisationServer
|
|
68
|
+
class AuthorisationServer:
|
|
54
69
|
authorisation_server_id: str
|
|
55
|
-
api_resources: List[
|
|
70
|
+
api_resources: List[ApiResource]
|
|
56
71
|
authorisation_server_certifications: List[AuthorisationServerCertification]
|
|
57
72
|
customer_friendly_description: str
|
|
58
73
|
customer_friendly_logo_uri: str
|
|
59
74
|
customer_friendly_name: str
|
|
60
75
|
developer_portal_uri: str
|
|
61
76
|
terms_of_service_uri: str
|
|
62
|
-
|
|
77
|
+
open_i_d_discovery_document: str
|
|
63
78
|
issuer: str
|
|
64
79
|
payload_signing_cert_location_uri: str
|
|
65
80
|
parent_authorisation_server_id: str
|
|
66
81
|
|
|
67
82
|
|
|
68
83
|
@dataclass
|
|
69
|
-
class OrgDomainRoleClaim
|
|
84
|
+
class OrgDomainRoleClaim:
|
|
70
85
|
status: str
|
|
71
86
|
authorisation_domain: str
|
|
72
87
|
role: str
|
|
@@ -97,7 +112,7 @@ class TokenSet:
|
|
|
97
112
|
|
|
98
113
|
|
|
99
114
|
@dataclass
|
|
100
|
-
class TokenInput
|
|
115
|
+
class TokenInput:
|
|
101
116
|
access_token: str
|
|
102
117
|
id_token: str
|
|
103
118
|
token_type: str
|
|
@@ -106,7 +121,7 @@ class TokenInput(JSONWizard):
|
|
|
106
121
|
|
|
107
122
|
|
|
108
123
|
@dataclass
|
|
109
|
-
class MtlsEndpointAliases
|
|
124
|
+
class MtlsEndpointAliases:
|
|
110
125
|
token_endpoint: Optional[str] = None
|
|
111
126
|
pushed_authorization_request_endpoint: Optional[str] = None
|
|
112
127
|
authorization_endpoint: Optional[str] = None
|
|
@@ -114,7 +129,7 @@ class MtlsEndpointAliases(JSONWizard):
|
|
|
114
129
|
|
|
115
130
|
|
|
116
131
|
@dataclass
|
|
117
|
-
class IssuerMetadata
|
|
132
|
+
class IssuerMetadata:
|
|
118
133
|
issuer: str
|
|
119
134
|
jwks_uri: str
|
|
120
135
|
id_token_signing_alg_values_supported: List[str]
|
|
@@ -126,20 +141,20 @@ class IssuerMetadata(JSONWizard):
|
|
|
126
141
|
|
|
127
142
|
def get_preferred_token_endpoint(self) -> str:
|
|
128
143
|
return self.mtls_endpoint_aliases.token_endpoint \
|
|
129
|
-
if self.mtls_endpoint_aliases.token_endpoint \
|
|
144
|
+
if self.mtls_endpoint_aliases and self.mtls_endpoint_aliases.token_endpoint \
|
|
130
145
|
else self.token_endpoint
|
|
131
146
|
|
|
132
147
|
def get_preferred_pushed_authorization_request_endpoint(self) -> str:
|
|
133
148
|
return self.mtls_endpoint_aliases.pushed_authorization_request_endpoint \
|
|
134
|
-
if self.mtls_endpoint_aliases.pushed_authorization_request_endpoint \
|
|
149
|
+
if self.mtls_endpoint_aliases and self.mtls_endpoint_aliases.pushed_authorization_request_endpoint \
|
|
135
150
|
else self.pushed_authorization_request_endpoint
|
|
136
151
|
|
|
137
152
|
def get_preferred_authorization_endpoint(self) -> str:
|
|
138
153
|
return self.mtls_endpoint_aliases.authorization_endpoint \
|
|
139
|
-
if self.mtls_endpoint_aliases.authorization_endpoint \
|
|
154
|
+
if self.mtls_endpoint_aliases and self.mtls_endpoint_aliases.authorization_endpoint \
|
|
140
155
|
else self.authorization_endpoint
|
|
141
156
|
|
|
142
157
|
def get_preferred_userinfo_endpoint(self) -> str:
|
|
143
158
|
return self.mtls_endpoint_aliases.userinfo_endpoint \
|
|
144
|
-
if self.mtls_endpoint_aliases.userinfo_endpoint \
|
|
159
|
+
if self.mtls_endpoint_aliases and self.mtls_endpoint_aliases.userinfo_endpoint \
|
|
145
160
|
else self.userinfo_endpoint
|
|
@@ -14,7 +14,11 @@ class RelyingPartyClientSdk:
|
|
|
14
14
|
self.config = config
|
|
15
15
|
|
|
16
16
|
if config.custom_config.enable_auto_compliance_verification:
|
|
17
|
-
logger.info(f"Auto Compliance Verification mode enabled, printing config information:
|
|
17
|
+
logger.info(f"Auto Compliance Verification mode enabled, printing config information: "
|
|
18
|
+
f"client_id={self.config.client_id}, "
|
|
19
|
+
f"signing_kid={self.config.signing_kid}, "
|
|
20
|
+
f"application_redirect_uri={self.config.application_redirect_uri}, "
|
|
21
|
+
f"registry_participants_uri={self.config.registry_participants_uri}")
|
|
18
22
|
|
|
19
23
|
def get_participants(self) -> list[Participant]:
|
|
20
24
|
return get_participants(self.config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rp_python_sdk-0.7.1 → 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
|