rp_python_sdk 0.3.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,98 @@
1
+ import time
2
+ from datetime import date
3
+
4
+ import requests
5
+ from rp_python_sdk.filters.participant_filters import remove_out_of_date_certifications, \
6
+ remove_unofficial_certifications, filter_for_fallback_identity_service_providers, \
7
+ remove_participants_without_auth_servers, remove_fallback_identity_service_provider, \
8
+ filter_auth_servers_for_supported_claims, filter_for_required_certifications, remove_inactive_auth_servers
9
+ from rp_python_sdk.model import IssuerMetadata, AuthorisationServer
10
+ from rp_python_sdk.model import Participant
11
+ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
12
+ from rp_python_sdk.sdk_config import SdkConfig
13
+ from rp_python_sdk.setup_logger import logger
14
+
15
+
16
+ def get_participants(config: SdkConfig) -> list[Participant]:
17
+
18
+ participants = _retrieve_full_participants_list(config)
19
+
20
+ filtered_participants = remove_fallback_identity_service_provider(participants)
21
+
22
+ if config.custom_config.include_uncertified_participants:
23
+ logger.info("Identity provider list has not been filtered as includeUncertifiedParticipants=true")
24
+ filtered_participants = remove_participants_without_auth_servers(filtered_participants)
25
+ return filtered_participants
26
+
27
+ filtered_participants = remove_out_of_date_certifications(filtered_participants, date.today())
28
+ filtered_participants = remove_unofficial_certifications(filtered_participants)
29
+
30
+ if len(config.custom_config.required_claims) != 0:
31
+ logger.debug(f"Identity provider list filtered for participants that support the "
32
+ f"following claims: {config.custom_config.required_claims}")
33
+ filtered_participants = filter_auth_servers_for_supported_claims(filtered_participants, config.custom_config.required_claims)
34
+
35
+ if len(config.custom_config.required_participant_certifications) != 0:
36
+ logger.debug(f"Identity provider list filtered for participants that support the "
37
+ f"following certifications: {config.custom_config.required_participant_certifications}")
38
+ filtered_participants = filter_for_required_certifications(filtered_participants, config.custom_config.required_participant_certifications)
39
+
40
+ filtered_participants = remove_inactive_auth_servers(filtered_participants)
41
+ filtered_participants = remove_participants_without_auth_servers(filtered_participants)
42
+
43
+ return filtered_participants
44
+
45
+ def retrieve_fallback_provider_participants(config: SdkConfig) -> list[Participant]:
46
+ participants = _retrieve_full_participants_list(config)
47
+
48
+ filtered_participants = remove_out_of_date_certifications(participants, date.today())
49
+ filtered_participants = remove_unofficial_certifications(filtered_participants)
50
+
51
+ filtered_participants = filter_for_fallback_identity_service_providers(filtered_participants)
52
+ filtered_participants = remove_participants_without_auth_servers(filtered_participants)
53
+
54
+ return filtered_participants
55
+
56
+
57
+ def _retrieve_full_participants_list(config: SdkConfig) -> list[Participant]:
58
+ logger.info(f"Retrieving all identity providers from {config.registry_participants_uri}")
59
+
60
+ response = requests.get(config.registry_participants_uri,
61
+ timeout=config.custom_config.timeout_in_seconds)
62
+ if response.status_code != 200:
63
+ raise RelyingPartyClientSdkException(
64
+ f"Call to identity provider endpoint failed with code: {response.status_code}")
65
+
66
+ participant_list: list[Participant] = Participant.from_json(response.text)
67
+
68
+ logger.info(f"Retrieved identity providers, num orgs found: {len(participant_list)}")
69
+ return participant_list
70
+
71
+
72
+ def get_auth_server_details(config: SdkConfig, authorisation_server_id: str) -> AuthorisationServer:
73
+ result = None
74
+ for participant in get_participants(config):
75
+ for server in participant.authorisation_servers:
76
+ if str(server.authorisation_server_id) == authorisation_server_id:
77
+ result = server
78
+ break
79
+
80
+ return result
81
+
82
+
83
+ def get_issuer_metadata(authorisation_server: AuthorisationServer):
84
+ auth_server_issuer = authorisation_server.open_id_discovery_document
85
+
86
+ try:
87
+ logger.info(f"Getting discovery document from {auth_server_issuer}")
88
+ response = requests.get(auth_server_issuer)
89
+ response.raise_for_status() # Raises an exception for 4XX or 5XX errors
90
+
91
+ return IssuerMetadata.from_json(response.text)
92
+
93
+ except requests.exceptions.HTTPError as http_err:
94
+ raise RelyingPartyClientSdkException(
95
+ f"Call to identity provider endpoint failed with code: {response.status_code}") from http_err
96
+ except requests.exceptions.RequestException as req_err:
97
+ raise RelyingPartyClientSdkException(
98
+ f"Failed to connect to authorisation service {auth_server_issuer}") from req_err
@@ -0,0 +1,204 @@
1
+ import json
2
+ import time
3
+ import uuid
4
+ from urllib.parse import quote
5
+
6
+ import requests
7
+ from authlib.common.security import generate_token
8
+ from authlib.jose import JsonWebSignature
9
+ from authlib.oauth2.rfc7636 import create_s256_code_challenge
10
+
11
+ from rp_python_sdk.endpoints.get_participants import get_auth_server_details, \
12
+ get_issuer_metadata
13
+ from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id, log_x_fapi_interaction_id_matches
14
+ from rp_python_sdk.model import PARResponse
15
+ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
16
+ from rp_python_sdk.sdk_config import SdkConfig
17
+ from rp_python_sdk.setup_logger import logger
18
+
19
+ default_claims = {"name", "given_name", "middle_name", "family_name", "phone_number", "email", "address", "birthdate",
20
+ "txn"}
21
+
22
+ extended_claims = {"over16", "over18", "over21", "over25", "over65", "beneficiary_account_au",
23
+ "beneficiary_account_au_payid", "beneficiary_account_international"}
24
+
25
+
26
+ def send_pushed_authorisation_request(config: SdkConfig,
27
+ authorisation_server_id: str,
28
+ essential_claims: set[str],
29
+ voluntary_claims: set[str],
30
+ purpose: str) -> PARResponse:
31
+ if len(essential_claims) == 0:
32
+ essential_claims = default_claims
33
+
34
+ _check_valid_purpose(purpose)
35
+
36
+ checked_essential_claims = _ensure_mandatory_claims_present(essential_claims, voluntary_claims)
37
+ claim_request = _generate_claims_request(checked_essential_claims, voluntary_claims)
38
+
39
+ authorisation_server = get_auth_server_details(config, authorisation_server_id)
40
+ issuer_metadata = get_issuer_metadata(authorisation_server)
41
+
42
+ auth_server_url = issuer_metadata.issuer
43
+ x_fapi_interaction_id = create_x_fapi_interaction_id()
44
+
45
+ logger.info(
46
+ f"Sending PAR to auth server: {authorisation_server_id} - {authorisation_server.customer_friendly_name}, "
47
+ f"essential claims requested: {checked_essential_claims}, voluntary claims requested: {voluntary_claims}, "
48
+ f"x-fapi-interaction-id: {x_fapi_interaction_id}, purpose: {purpose}")
49
+
50
+ par_endpoint = issuer_metadata.get_preferred_pushed_authorization_request_endpoint()
51
+ auth_endpoint = issuer_metadata.get_preferred_authorization_endpoint()
52
+ redirect_uri = config.application_redirect_uri
53
+ client_id = config.client_id
54
+
55
+ current_time_in_seconds = int(time.time())
56
+
57
+ iat = current_time_in_seconds
58
+ exp = current_time_in_seconds + 300
59
+
60
+ state = generate_token()
61
+ nonce = generate_token()
62
+ scope = "openid"
63
+ jti = str(uuid.uuid4())
64
+
65
+ code_verifier = generate_token(48)
66
+ code_challenge = create_s256_code_challenge(code_verifier)
67
+
68
+ header = {'alg': 'PS256', 'kid': config.signing_kid}
69
+ request_payload = {
70
+ 'iss': client_id,
71
+ 'aud': auth_server_url,
72
+ 'exp': exp,
73
+ 'nbf': iat,
74
+ 'response_type': 'code',
75
+ 'code_challenge_method': 'S256',
76
+ 'nonce': nonce,
77
+ 'client_id': client_id,
78
+ 'scope': scope,
79
+ 'claims': claim_request,
80
+ 'redirect_uri': redirect_uri,
81
+ 'state': state,
82
+ 'prompt': 'consent',
83
+ 'code_challenge': code_challenge,
84
+ 'purpose': purpose,
85
+ }
86
+
87
+ jws = JsonWebSignature()
88
+ request_jwt = jws.serialize_compact(header, json.dumps(request_payload), config.signing_key)
89
+
90
+ client_assertion_payload = {
91
+ 'sub': client_id,
92
+ 'aud': auth_server_url,
93
+ 'iss': client_id,
94
+ 'exp': exp,
95
+ 'jti': jti,
96
+ 'iat': iat,
97
+ }
98
+
99
+ client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload),
100
+ config.signing_key)
101
+
102
+ # Data payload
103
+ data = {
104
+ "request": request_jwt.decode("utf-8"),
105
+ "client_assertion": client_assertion_jwt.decode("utf-8"),
106
+ "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
107
+ }
108
+
109
+ # Custom headers
110
+ headers = {
111
+ "accept": "application/json",
112
+ "x-fapi-interaction-id": x_fapi_interaction_id
113
+ }
114
+
115
+ # Making the POST request
116
+ response = requests.post(par_endpoint, data=data, headers=headers,
117
+ cert=(config.transport_pem, config.transport_key),
118
+ verify=config.ca_pem)
119
+
120
+ log_x_fapi_interaction_id_matches(authorisation_server, "send_pushed_authorisation_request", x_fapi_interaction_id, response)
121
+
122
+ request_uri = response.json()['request_uri']
123
+ logger.debug(f"PAR was sent, request_uri='{request_uri}'")
124
+
125
+ return PARResponse(_construct_auth_url(auth_endpoint, client_id, request_uri),
126
+ code_verifier, state, nonce, x_fapi_interaction_id)
127
+
128
+
129
+ def _check_valid_purpose(purpose) -> None:
130
+ if len(purpose) < 3 or len(purpose) > 50:
131
+ raise RelyingPartyClientSdkException("Purpose must be between 3 and 50 characters")
132
+
133
+ illegal_chars = ["<", ">", "(", ")", "{", "}", "'", "\\"]
134
+
135
+ contains_illegal_char = any(char in purpose for char in illegal_chars)
136
+ if contains_illegal_char:
137
+ raise RelyingPartyClientSdkException(
138
+ f"Purpose cannot contain any of the following characters: {', '.join(illegal_chars)}, purpose supplied: [{purpose}]")
139
+
140
+
141
+ def _ensure_mandatory_claims_present(essential_claims: set[str], voluntary_claims: set[str]) -> set[str]:
142
+ if "txn" in essential_claims or "txn" in voluntary_claims:
143
+ return essential_claims
144
+
145
+ # Create a copy of the essential_claims to avoid modifying the original set
146
+ result_claims = essential_claims.copy()
147
+ # Add "txn" to the set
148
+ result_claims.add("txn")
149
+ return result_claims
150
+
151
+
152
+ def _generate_claims_request(essential_claims: set[str], voluntary_claims: set[str]):
153
+ # Filter out any claims that are in both essential and voluntary sets so essential takes precedence
154
+ deduplicated_voluntary_claims = voluntary_claims - essential_claims
155
+
156
+ # Create maps for essential and voluntary claims
157
+ essential = {"essential": True}
158
+ voluntary = {"essential": False}
159
+
160
+ # Partition claims into basic and extended types
161
+ partitioned_essential_claims = _partition_claims(essential_claims)
162
+ partitioned_voluntary_claims = _partition_claims(deduplicated_voluntary_claims)
163
+
164
+ # Map basic claims to their respective maps
165
+ id_token_claims = _map_claims(partitioned_essential_claims[False], essential)
166
+ id_token_claims.update(_map_claims(partitioned_voluntary_claims[False], voluntary))
167
+
168
+ # Map extended claims to their respective maps
169
+ extended_claims = _map_claims(partitioned_essential_claims[True], essential)
170
+ extended_claims.update(_map_claims(partitioned_voluntary_claims[True], voluntary))
171
+
172
+ # Add extended claims to idTokenClaims if any exist
173
+ if extended_claims:
174
+ id_token_claims["verified_claims"] = {
175
+ "verification": {"trust_framework": {"value": "au_connectid"}},
176
+ "claims": extended_claims
177
+ }
178
+
179
+ return {"id_token": id_token_claims}
180
+
181
+
182
+ def _partition_claims(claims: set[str]):
183
+ """
184
+ Partition claims into basic and extended types based on a criteria (here, whether they are in extended_claims).
185
+ """
186
+ partitioned: dict[bool, set[str]] = {True: set(), False: set()}
187
+ for claim in claims:
188
+ partitioned[claim in extended_claims].add(claim)
189
+ return partitioned
190
+
191
+
192
+ def _map_claims(claims, claim_map):
193
+ """
194
+ Map each claim to the provided claim_map.
195
+ """
196
+ return {claim: claim_map for claim in claims}
197
+
198
+
199
+ def _construct_auth_url(auth_endpoint: str, client_id: str, request_uri: str) -> str:
200
+ return (
201
+ f"{auth_endpoint}"
202
+ f"?client_id={quote(client_id)}"
203
+ f"&request_uri={quote(request_uri)}"
204
+ )
@@ -0,0 +1,254 @@
1
+ import base64
2
+ import json
3
+ import time
4
+ import uuid
5
+ from typing import Tuple, Any
6
+
7
+ import requests
8
+ from authlib.jose import JsonWebSignature
9
+
10
+ from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
11
+ get_auth_server_details
12
+ from rp_python_sdk.endpoints.user_info import call_user_info
13
+ from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id, log_x_fapi_interaction_id_matches
14
+ from rp_python_sdk.model import CallbackBody, TokenSet, TokenInput, IssuerMetadata
15
+ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
16
+ from rp_python_sdk.sdk_config import SdkConfig
17
+ from rp_python_sdk.setup_logger import logger
18
+
19
+
20
+ def retrieve_tokens(config: SdkConfig, authorisation_server_id: str, callback_body: CallbackBody,
21
+ original_code_verifier: str,
22
+ original_state: str, nonce: str) -> TokenSet:
23
+ authorisation_server = get_auth_server_details(config, authorisation_server_id)
24
+ issuer_metadata = get_issuer_metadata(authorisation_server)
25
+
26
+ if issuer_metadata.issuer != callback_body.iss:
27
+ raise RelyingPartyClientSdkException(
28
+ f"Issuer returned from authorization endpoint ({callback_body.iss} does not match issuer defined in authorization server well-known ({issuer_metadata.issuer})")
29
+
30
+ x_fapi_interaction_id = create_x_fapi_interaction_id()
31
+ logger.info(
32
+ f"Retrieving token response from authorisation server: {authorisation_server_id} - {authorisation_server.customer_friendly_name}, x-fapi-interaction-id: {x_fapi_interaction_id}")
33
+
34
+ token_endpoint = issuer_metadata.get_preferred_token_endpoint()
35
+
36
+ if callback_body.state != original_state:
37
+ raise RelyingPartyClientSdkException(
38
+ f"Callback response state value does not match state, callback state: {callback_body.state}, state {original_state}, x-fapi-interaction-id: {x_fapi_interaction_id}")
39
+
40
+ if callback_body.iss != issuer_metadata.issuer:
41
+ raise RelyingPartyClientSdkException(
42
+ f"iss mismatch, expected {issuer_metadata.issuer} , got: {callback_body.iss}")
43
+
44
+ redirect_uri = config.application_redirect_uri
45
+ client_id = config.client_id
46
+ current_time_in_seconds = int(time.time())
47
+ iat = current_time_in_seconds
48
+ exp = current_time_in_seconds + 300
49
+ jti = str(uuid.uuid4())
50
+
51
+ client_assertion_payload = {
52
+ 'sub': client_id,
53
+ 'aud': token_endpoint,
54
+ 'iss': client_id,
55
+ 'exp': exp,
56
+ 'jti': jti,
57
+ 'iat': iat,
58
+ }
59
+
60
+ jws = JsonWebSignature()
61
+ header = {'alg': 'PS256', 'kid': config.signing_kid}
62
+
63
+ client_assertion_jwt = jws.serialize_compact(header, json.dumps(client_assertion_payload),
64
+ config.signing_key)
65
+
66
+ # Data payload
67
+ data = {
68
+ "grant_type": "authorization_code",
69
+ "code": callback_body.code,
70
+ "redirect_uri": redirect_uri,
71
+ "client_assertion": client_assertion_jwt.decode("utf-8"),
72
+ "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
73
+ "code_verifier": original_code_verifier
74
+ }
75
+
76
+ # Custom headers
77
+ headers = {
78
+ "accept": "application/json",
79
+ "x-fapi-interaction-id": x_fapi_interaction_id
80
+ }
81
+
82
+ # Making the POST request
83
+ response = requests.post(token_endpoint, data=data, headers=headers,
84
+ cert=(config.transport_pem, config.transport_key),
85
+ verify=config.ca_pem)
86
+
87
+ log_x_fapi_interaction_id_matches(authorisation_server, "retrieve_tokens", x_fapi_interaction_id, response)
88
+
89
+ token_input = TokenInput.from_json(response.text)
90
+
91
+ jwks_response_body = _get_jwks_json(issuer_metadata, x_fapi_interaction_id)
92
+ allowed_algs = issuer_metadata.id_token_signing_alg_values_supported
93
+
94
+ header, payload = _validate(token_input, jwks_response_body, allowed_algs, nonce, client_id, callback_body)
95
+
96
+ token_set = TokenSet(token_input, payload, x_fapi_interaction_id)
97
+
98
+ if config.custom_config.enable_auto_compliance_verification:
99
+ call_user_info(config, authorisation_server_id, token_set.token_input.access_token)
100
+
101
+ logger.info(
102
+ f"Token response successfully retrieved from authorisation server {authorisation_server_id}, x-fapi-interaction-id: {x_fapi_interaction_id}, txn: {token_set.claims['txn']}")
103
+
104
+ return token_set
105
+
106
+
107
+ def _validate(token_input: TokenInput, jwks_response_body: dict[str, Any], allowed_algs: list[str],
108
+ nonce: str, client_id: str,
109
+ callback_body: CallbackBody) -> Tuple[dict[str, Any], dict[str, Any]]:
110
+ """
111
+ Decode and parse an ID token into JSON objects for header and payload.
112
+
113
+ :param token_input: A dictionary containing an 'idToken' key, which is a Base64 encoded string
114
+ :param jwks_response_body
115
+ :param allowed_algs
116
+ :param nonce
117
+ :param client_id
118
+ :param callback_body
119
+ :return: two dictionaries for header and payload
120
+ """
121
+ id_token = token_input.id_token
122
+ header_base64 = id_token.split('.')[0]
123
+ header = _decode_base64(header_base64)
124
+ header_json = json.loads(header)
125
+
126
+ payload_base64 = id_token.split('.')[1]
127
+ payload = _decode_base64(payload_base64)
128
+ payload_json = json.loads(payload)
129
+
130
+ _validate_header(header_json, jwks_response_body, allowed_algs, id_token)
131
+ _validate_payload(payload_json, nonce, callback_body, client_id)
132
+
133
+ return header_json, payload_json
134
+
135
+
136
+ def _validate_payload(json_payload: dict[str, Any], nonce: str, callback_body: CallbackBody,
137
+ client_id: str) -> None:
138
+ if 'nonce' not in json_payload:
139
+ raise RelyingPartyClientSdkException('nonce claim missing from token response')
140
+
141
+ if json_payload['nonce'] != nonce:
142
+ raise RelyingPartyClientSdkException(
143
+ f"nonce returned in ID token: {json_payload['nonce']} does not match provided nonce: {nonce}")
144
+
145
+ current_time_stamp = int(time.time())
146
+
147
+ if 'iss' not in json_payload:
148
+ raise RelyingPartyClientSdkException('iss claim missing from token response')
149
+
150
+ if 'iat' not in json_payload:
151
+ raise RelyingPartyClientSdkException('iat claim missing from token response')
152
+
153
+ if 'aud' not in json_payload:
154
+ raise RelyingPartyClientSdkException('aud claim missing from token response')
155
+
156
+ if 'exp' not in json_payload:
157
+ raise RelyingPartyClientSdkException('exp claim missing from token response')
158
+
159
+ # Check if iat claim is older than 10 minutes ago
160
+ if json_payload['iat'] < current_time_stamp - 600:
161
+ raise RelyingPartyClientSdkException('iat claim in token response is too old')
162
+
163
+ # Check exp claim is older than current epoch (allowing for 5 minute skew)
164
+ if json_payload['exp'] < current_time_stamp - 300:
165
+ raise RelyingPartyClientSdkException('id token expired more than 5 minutes ago')
166
+
167
+ # Check the iss claim is valid
168
+ if json_payload['iss'] != callback_body.iss:
169
+ raise RelyingPartyClientSdkException(
170
+ f'iss claim in token response "{json_payload["iss"]}" does not match the expected value of "{callback_body.iss}"')
171
+
172
+ # Extract aud claim from either list or string
173
+ if isinstance(json_payload['aud'], list):
174
+ for aud_element in json_payload['aud']:
175
+ if aud_element != client_id:
176
+ raise RelyingPartyClientSdkException(
177
+ 'one of the aud claim elements in token response does not match client ID')
178
+ else:
179
+ if json_payload['aud'] != client_id:
180
+ raise RelyingPartyClientSdkException('aud claim in token response does not match client ID')
181
+
182
+
183
+ def _decode_base64(base64_str: str) -> str:
184
+ """
185
+ Decode a Base64 encoded string.
186
+ :param base64_str: Base64 encoded string
187
+ :return: decoded string
188
+ """
189
+ decoded_bytes = base64.b64decode(base64_str + '==')
190
+ return decoded_bytes.decode('utf-8')
191
+
192
+
193
+ def _validate_header(json_header, jwks, allowed_algs, id_token: str) -> None:
194
+ if 'alg' not in json_header or json_header['alg'] == 'none':
195
+ raise RelyingPartyClientSdkException("no alg value in the token response jwt header")
196
+
197
+ if 'kid' not in json_header:
198
+ raise RelyingPartyClientSdkException("no kid value in the token response jwt header")
199
+
200
+ kid = json_header['kid']
201
+ jwk = None
202
+
203
+ for key_element in jwks.get('keys', []):
204
+ if _matching_kid(kid, key_element) and _none_or_matching_alg(json_header, key_element):
205
+ jwk = key_element
206
+ break
207
+
208
+ if jwk is None:
209
+ raise RelyingPartyClientSdkException(
210
+ f"kid provided does not match any key in the jwks for specified algorithm in token header: {json_header['alg']}")
211
+
212
+ found_alg = any(alg.lower() == json_header['alg'].lower() for alg in allowed_algs)
213
+ if not found_alg:
214
+ raise RelyingPartyClientSdkException(
215
+ "alg value in id token header is not one of the supported algorithms listed on the well-known")
216
+
217
+ # try:
218
+ # validate_signature(token_input.get('id_token'), jwk)
219
+ # except (JOSEError, ValueError) as e:
220
+ # raise RelyingPartyClientSdkException(f"exception while validation jwt signature: {e}")
221
+
222
+
223
+ def _validate_signature(self, id_token, jwk):
224
+ # Implement JWT signature validation based on the jwk provided and the library you're using
225
+ pass
226
+
227
+
228
+ def _matching_kid(kid: str, key_element) -> bool:
229
+ # Implement this to match the 'kid' from the JWT with one from the JWKS
230
+ return key_element.get('kid') == kid
231
+
232
+
233
+ def _none_or_matching_alg(json_header, key_element) -> bool:
234
+ # Implement this to check if the 'alg' in JWT matches the one in the key element, if required
235
+ return True
236
+
237
+
238
+ def _get_jwks_json(issuer_metadata: IssuerMetadata, x_fapi_interaction_id: str) -> Any:
239
+ headers = {
240
+ "Accept": "application/json",
241
+ "x-fapi-interaction-id": x_fapi_interaction_id
242
+ }
243
+ response = requests.get(issuer_metadata.jwks_uri, headers=headers)
244
+ response.raise_for_status()
245
+
246
+ if not _is_success(response):
247
+ raise RelyingPartyClientSdkException(
248
+ f"Response code for JWKS to {issuer_metadata.jwks_uri} not successful, was: {response.status_code}, x-fapi-interaction-id: {x_fapi_interaction_id}, response body: {response.text}")
249
+
250
+ return response.json()
251
+
252
+
253
+ def _is_success(response: requests.Response):
254
+ return 200 <= response.status_code < 300
@@ -0,0 +1,44 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+
5
+ from rp_python_sdk.endpoints.get_participants import get_issuer_metadata, \
6
+ get_auth_server_details
7
+ from rp_python_sdk.endpoints.util.fapi import create_x_fapi_interaction_id
8
+ from rp_python_sdk.relying_party_client_sdk_exception import RelyingPartyClientSdkException
9
+ from rp_python_sdk.sdk_config import SdkConfig
10
+ from rp_python_sdk.setup_logger import logger
11
+
12
+
13
+ def call_user_info(config: SdkConfig, authorisation_server_id: str, access_token: str) -> dict[str, Any]:
14
+ x_fapi_interaction_id = create_x_fapi_interaction_id()
15
+ logger.info(f"Making call to user info endpoint, x-fapi-interaction-id: {x_fapi_interaction_id}")
16
+
17
+ authorisation_server = get_auth_server_details(config, authorisation_server_id)
18
+ issuer_metadata = get_issuer_metadata(authorisation_server)
19
+
20
+ user_info_endpoint = issuer_metadata.get_preferred_userinfo_endpoint()
21
+
22
+ headers = {
23
+ "Authorization": f"Bearer {access_token}",
24
+ "x-fapi-interaction-id": x_fapi_interaction_id
25
+ }
26
+
27
+ try:
28
+ response = requests.get(user_info_endpoint, headers=headers,
29
+ cert=(config.transport_pem, config.transport_key),
30
+ verify=config.ca_pem)
31
+
32
+ if not _is_success(response):
33
+ raise RelyingPartyClientSdkException(
34
+ f"Call to user info endpoint failed with code: {response.status_code}")
35
+
36
+ return response.json()
37
+
38
+ except requests.exceptions.RequestException as e:
39
+ raise RelyingPartyClientSdkException(
40
+ f"Failed to read body from response while connecting to {user_info_endpoint}") from e
41
+
42
+
43
+ def _is_success(response: requests.Response):
44
+ return 200 <= response.status_code < 300
@@ -0,0 +1,30 @@
1
+ import uuid
2
+
3
+ from requests.models import Response
4
+
5
+ from rp_python_sdk.model import AuthorisationServer
6
+ from rp_python_sdk.setup_logger import logger
7
+
8
+
9
+ def create_x_fapi_interaction_id():
10
+ return str(uuid.uuid4())
11
+
12
+
13
+ def log_x_fapi_interaction_id_matches(authorisation_server: AuthorisationServer, request_name: str,
14
+ x_fapi_interaction_id: str, response: Response):
15
+ fapi_header = response.headers.get('x-fapi-interaction-id')
16
+ if not fapi_header:
17
+ logger.warning(
18
+ f"No x-fapi-interaction-id header returned from auth server {authorisation_server.authorisation_server_id} "
19
+ f"on request {request_name}, expected {x_fapi_interaction_id}")
20
+ return
21
+
22
+ if x_fapi_interaction_id == fapi_header:
23
+ logger.debug(
24
+ f"x-fapi-interaction-id correctly returned from auth server {authorisation_server.authorisation_server_id} "
25
+ f"on {request_name} matched expected {x_fapi_interaction_id}")
26
+ return
27
+
28
+ logger.warning(
29
+ f"x-fapi-interaction-id returned from auth server {authorisation_server.authorisation_server_id} "
30
+ f"on {request_name} did not match. sent {x_fapi_interaction_id}, returned {fapi_header}")