From 32476b3f62305aac705a5de8beea8d35f5ba1c91 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Sun, 10 Sep 2023 08:26:32 +0200 Subject: [PATCH 01/39] More appropriate function name. Only need to get the authentication method names once. --- src/idpyoidc/server/client_authn.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/idpyoidc/server/client_authn.py b/src/idpyoidc/server/client_authn.py index 33043077..119b80f4 100755 --- a/src/idpyoidc/server/client_authn.py +++ b/src/idpyoidc/server/client_authn.py @@ -435,10 +435,11 @@ def _verify( TYPE_METHOD = [(JWT_BEARER, JWSAuthnMethod)] -def valid_client_info(cinfo): - eta = cinfo.get("client_secret_expires_at", 0) - if eta != 0 and eta < utc_time_sans_frac(): - return False +def valid_client_secret(cinfo): + if "client_secret" in cinfo: + eta = cinfo.get("client_secret_expires_at", 0) + if eta != 0 and eta < utc_time_sans_frac(): + return False return True @@ -472,10 +473,8 @@ def verify_client( auth_info = {} - methods = endpoint.client_authn_method - if not methods: - _context = endpoint.upstream_get("context") - methods = _context.client_authn_methods + _context = endpoint.upstream_get("context") + methods = _context.client_authn_methods client_id = None allowed_methods = getattr(endpoint, "client_authn_method") @@ -524,8 +523,8 @@ def verify_client( if not _cinfo: raise UnknownClient("Unknown Client ID") - if not valid_client_info(_cinfo): - logger.warning("Client registration has timed out or " "client secret is expired.") + if not valid_client_secret(_cinfo): + logger.warning("Client secret has expired.") raise InvalidClient("Not valid client") # Validate that the used method is allowed for this client/endpoint From 12676323cc110f594b234bdd497f046caeef77a3 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Mon, 11 Sep 2023 09:25:57 +0200 Subject: [PATCH 02/39] There are occasions when it's useful to know under which umbrella an endpoint is working. Whether it's OIDC or OAuth2 or ... --- src/idpyoidc/server/endpoint.py | 1 + src/idpyoidc/server/oauth2/authorization.py | 16 +++++++++++----- src/idpyoidc/server/oauth2/introspection.py | 1 + .../server/oauth2/pushed_authorization.py | 1 + src/idpyoidc/server/oauth2/server_metadata.py | 1 + src/idpyoidc/server/oauth2/token.py | 1 + src/idpyoidc/server/oauth2/token_revocation.py | 2 ++ src/idpyoidc/server/oidc/authorization.py | 1 + .../server/oidc/backchannel_authentication.py | 1 + src/idpyoidc/server/oidc/discovery.py | 1 + src/idpyoidc/server/oidc/provider_config.py | 1 + src/idpyoidc/server/oidc/read_registration.py | 1 + src/idpyoidc/server/oidc/registration.py | 1 + src/idpyoidc/server/oidc/session.py | 1 + src/idpyoidc/server/oidc/token.py | 1 + src/idpyoidc/server/oidc/userinfo.py | 4 +++- tests/private/token_jwks.json | 2 +- tests/pub_client.jwks | 2 +- tests/pub_iss.jwks | 2 +- 19 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index 3abc5343..1c3d7119 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -84,6 +84,7 @@ class Endpoint(Node): error_cls = ResponseMessage endpoint_name = "" endpoint_path = "" + endpoint_type = "" name = "" request_format = "urlencoded" request_placement = "query" diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index ccacde08..6adac3aa 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -98,6 +98,7 @@ def verify_uri( request: Union[dict, Message], uri_type: str, client_id: Optional[str] = None, + endpoint_type: Optional[str] = 'oidc' ): """ A redirect URI @@ -139,7 +140,8 @@ def verify_uri( redirect_uris = client_info.get(f"{uri_type}") if redirect_uris is None: - raise RedirectURIError(f"No registered {uri_type} for {_cid}") + if endpoint_type == "oidc": + raise RedirectURIError(f"No registered {uri_type} for {_cid}") else: match = False for _item in redirect_uris: @@ -195,7 +197,10 @@ def join_query(base, query): return base -def get_uri(context, request, uri_type): +def get_uri(context, + request: Union[Message, dict], + uri_type: str, + endpoint_type: Optional[str] = "oidc"): """verify that the redirect URI is reasonable. :param context: An EndpointContext instance @@ -206,7 +211,7 @@ def get_uri(context, request, uri_type): uri = "" if uri_type in request: - verify_uri(context, request, uri_type) + verify_uri(context, request, uri_type,endpoint_type=endpoint_type) uri = request[uri_type] else: uris = f"{uri_type}s" @@ -341,6 +346,7 @@ class Authorization(Endpoint): response_placement = "url" endpoint_name = "authorization_endpoint" name = "authorization" + endpoint_type = "oauth2" _supports = { "claims_parameter_supported": True, @@ -509,7 +515,7 @@ def _post_parse_request(self, request, client_id, context, **kwargs): # Get a verified redirect URI try: - redirect_uri = get_uri(context, request, "redirect_uri") + redirect_uri = get_uri(context, request, "redirect_uri", self.endpoint_type) except (RedirectURIError, ParameterError) as err: return self.authentication_error_response( request, @@ -977,7 +983,7 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** logger.debug("Known clients: {}".format(list(_context.cdb.keys()))) try: - redirect_uri = get_uri(_context, request, "redirect_uri") + redirect_uri = get_uri(_context, request, "redirect_uri", self.endpoint_type) except (RedirectURIError, ParameterError) as err: return self.error_response( response_info, request, "invalid_request", "{}".format(err.args) diff --git a/src/idpyoidc/server/oauth2/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index 5937d0d5..6c64055b 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -19,6 +19,7 @@ class Introspection(Endpoint): request_format = "urlencoded" response_format = "json" endpoint_name = "introspection_endpoint" + endpoint_type = "oauth2" name = "introspection" _supports = { "client_authn_method": [ diff --git a/src/idpyoidc/server/oauth2/pushed_authorization.py b/src/idpyoidc/server/oauth2/pushed_authorization.py index d71c6b34..83aa8c1f 100644 --- a/src/idpyoidc/server/oauth2/pushed_authorization.py +++ b/src/idpyoidc/server/oauth2/pushed_authorization.py @@ -17,6 +17,7 @@ class PushedAuthorization(Authorization): response_placement = "body" response_format = "json" name = "pushed_authorization" + endpoint_type = "oauth2" def __init__(self, upstream_get, **kwargs): Authorization.__init__(self, upstream_get, **kwargs) diff --git a/src/idpyoidc/server/oauth2/server_metadata.py b/src/idpyoidc/server/oauth2/server_metadata.py index 2f9cea10..544339e9 100755 --- a/src/idpyoidc/server/oauth2/server_metadata.py +++ b/src/idpyoidc/server/oauth2/server_metadata.py @@ -12,6 +12,7 @@ class ServerMetadata(Endpoint): request_format = "" response_format = "json" name = "server_metadata" + endpoint_type = "oauth2" def __init__(self, upstream_get, **kwargs): Endpoint.__init__(self, upstream_get=upstream_get, **kwargs) diff --git a/src/idpyoidc/server/oauth2/token.py b/src/idpyoidc/server/oauth2/token.py index c6a53d1c..ab4485f6 100755 --- a/src/idpyoidc/server/oauth2/token.py +++ b/src/idpyoidc/server/oauth2/token.py @@ -34,6 +34,7 @@ class Token(Endpoint): name = "token" default_capabilities = {"token_endpoint_auth_signing_alg_values_supported": None} token_exchange_helper = TokenExchangeHelper + endpoint_type = "oauth2" helper_by_grant_type = { "authorization_code": AccessTokenHelper, diff --git a/src/idpyoidc/server/oauth2/token_revocation.py b/src/idpyoidc/server/oauth2/token_revocation.py index d36ed28b..80e20e35 100644 --- a/src/idpyoidc/server/oauth2/token_revocation.py +++ b/src/idpyoidc/server/oauth2/token_revocation.py @@ -22,7 +22,9 @@ class TokenRevocation(Endpoint): response_format = "text" response_body_type = "text" endpoint_name = "revocation_endpoint" + endpoint_type = "oauth2" name = "token_revocation" + default_capabilities = { "client_authn_method": [ "client_secret_basic", diff --git a/src/idpyoidc/server/oidc/authorization.py b/src/idpyoidc/server/oidc/authorization.py index ccca8e3b..dfbda6cd 100644 --- a/src/idpyoidc/server/oidc/authorization.py +++ b/src/idpyoidc/server/oidc/authorization.py @@ -76,6 +76,7 @@ class Authorization(authorization.Authorization): response_placement = "url" endpoint_name = "authorization_endpoint" name = "authorization" + endpoint_type = "oidc" _supports = { **authorization.Authorization._supports, diff --git a/src/idpyoidc/server/oidc/backchannel_authentication.py b/src/idpyoidc/server/oidc/backchannel_authentication.py index b94dbdf0..32797cff 100644 --- a/src/idpyoidc/server/oidc/backchannel_authentication.py +++ b/src/idpyoidc/server/oidc/backchannel_authentication.py @@ -35,6 +35,7 @@ class BackChannelAuthentication(Endpoint): response_placement = "url" endpoint_name = "backchannel_authentication_endpoint" name = "backchannel_authentication" + endpoint_type = "oidc" _supports = { "backchannel_token_delivery_modes_supported": ["poll", "ping", "push"], diff --git a/src/idpyoidc/server/oidc/discovery.py b/src/idpyoidc/server/oidc/discovery.py index 70767c75..a57c583f 100755 --- a/src/idpyoidc/server/oidc/discovery.py +++ b/src/idpyoidc/server/oidc/discovery.py @@ -12,6 +12,7 @@ class Discovery(Endpoint): request_format = "urlencoded" response_format = "json" name = "discovery" + endpoint_type = "oidc" def do_response(self, response_args=None, request=None, **kwargs): """ diff --git a/src/idpyoidc/server/oidc/provider_config.py b/src/idpyoidc/server/oidc/provider_config.py index 51f9a9d4..819a6997 100755 --- a/src/idpyoidc/server/oidc/provider_config.py +++ b/src/idpyoidc/server/oidc/provider_config.py @@ -12,6 +12,7 @@ class ProviderConfiguration(Endpoint): request_format = "" response_format = "json" name = "provider_config" + endpoint_type = "oidc" def __init__(self, upstream_get, **kwargs): Endpoint.__init__(self, upstream_get=upstream_get, **kwargs) diff --git a/src/idpyoidc/server/oidc/read_registration.py b/src/idpyoidc/server/oidc/read_registration.py index e824415c..c74332e4 100644 --- a/src/idpyoidc/server/oidc/read_registration.py +++ b/src/idpyoidc/server/oidc/read_registration.py @@ -13,6 +13,7 @@ class RegistrationRead(Endpoint): request_placement = "url" response_format = "json" name = "registration_read" + endpoint_type = "oidc" def get_client_id_from_token(self, context, token, request=None): if "client_id" in request: diff --git a/src/idpyoidc/server/oidc/registration.py b/src/idpyoidc/server/oidc/registration.py index 5437f74e..e7d34927 100644 --- a/src/idpyoidc/server/oidc/registration.py +++ b/src/idpyoidc/server/oidc/registration.py @@ -124,6 +124,7 @@ class Registration(Endpoint): response_format = "json" endpoint_name = "registration_endpoint" name = "registration" + endpoint_type = "oidc" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/idpyoidc/server/oidc/session.py b/src/idpyoidc/server/oidc/session.py index ee1a8460..9e6071e6 100644 --- a/src/idpyoidc/server/oidc/session.py +++ b/src/idpyoidc/server/oidc/session.py @@ -80,6 +80,7 @@ class Session(Endpoint): response_placement = "url" endpoint_name = "end_session_endpoint" name = "session" + endpoint_type = "oidc" _supports = { "frontchannel_logout_supported": True, diff --git a/src/idpyoidc/server/oidc/token.py b/src/idpyoidc/server/oidc/token.py index 101a9b4b..04f40a86 100755 --- a/src/idpyoidc/server/oidc/token.py +++ b/src/idpyoidc/server/oidc/token.py @@ -27,6 +27,7 @@ class Token(token.Token): endpoint_name = "token_endpoint" name = "token" default_capabilities = None + endpoint_type = "oidc" _supports = { "token_endpoint_auth_methods_supported": [ diff --git a/src/idpyoidc/server/oidc/userinfo.py b/src/idpyoidc/server/oidc/userinfo.py index c4d76596..2d8f624b 100755 --- a/src/idpyoidc/server/oidc/userinfo.py +++ b/src/idpyoidc/server/oidc/userinfo.py @@ -8,8 +8,8 @@ from cryptojwt.exception import MissingValue from cryptojwt.jwt import JWT from cryptojwt.jwt import utc_time_sans_frac -from idpyoidc import metadata +from idpyoidc import metadata from idpyoidc.exception import ImproperlyConfigured from idpyoidc.message import Message from idpyoidc.message import oidc @@ -30,6 +30,8 @@ class UserInfo(Endpoint): response_placement = "body" endpoint_name = "userinfo_endpoint" name = "userinfo" + endpoint_type = "oidc" + _supports = { "claim_types_supported": ["normal", "aggregated", "distributed"], "encrypt_userinfo_supported": True, diff --git a/tests/private/token_jwks.json b/tests/private/token_jwks.json index 105acf1b..96348945 100644 --- a/tests/private/token_jwks.json +++ b/tests/private/token_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "GupTpe3QRVG7tC6UFJShLi3i9L8gznvU"}]} \ No newline at end of file +{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "XB2_T04TbhR_hmpm439FntWuuidEDy-H"}]} \ No newline at end of file diff --git a/tests/pub_client.jwks b/tests/pub_client.jwks index d5ce25ed..84a27042 100644 --- a/tests/pub_client.jwks +++ b/tests/pub_client.jwks @@ -1 +1 @@ -{"keys": [{"kty": "EC", "use": "sig", "kid": "azZQQ2FEQjh3QnVZWVdrbHJkMEZSaWR6aVJ0LTBjeUFfeWRlbTRrRFZ5VQ", "crv": "P-256", "x": "2ADe18caWWGp6hpRbfa9HqQHDFNpid9xUmR56Wzm_wc", "y": "HnD_8QBanz4Y-UF8mKQFZXfqkGkXUSm34mLsdDKtSyk"}, {"kty": "RSA", "use": "sig", "kid": "SHEyYWcwNVk0LTdROTZzZ2FUWndIVXdack0xWUM5SEpwcS03dVUxWU4zRQ", "n": "rRz52ddyP9Y2ezSlRsnkt-sjXfV_Ii7vOFX-cStLE3IUlVeSJGEe_kAASLr2r3BE2unjntaxj67NP8D95h_rzG1SpCklTEn-aTe3FOwNyTzUH_oiDVeRoEcf04Y43ciRGYRB5PhI6ii-2lYuig6hyUr776Qxiu6-0zw-M_ay2MgGSy5CEj55dDSvcUyxStUObxGpPWnEvybO1vnE7iJEWGNe0L5uPe5nLidOiR-JwjxSWEx1xZYtIjxaf2Ulu-qu4hwgwBUQdx4bNZyBfljKj55skWuHqPMG3xMjnedQC6Ms5bR3rIkbBpvmgI3kJK-4CZikM6ruyLo94-Lk19aYQw", "e": "AQAB"}]} \ No newline at end of file +{"keys": [{"kty": "EC", "use": "sig", "kid": "azZQQ2FEQjh3QnVZWVdrbHJkMEZSaWR6aVJ0LTBjeUFfeWRlbTRrRFZ5VQ", "crv": "P-256", "x": "2ADe18caWWGp6hpRbfa9HqQHDFNpid9xUmR56Wzm_wc", "y": "HnD_8QBanz4Y-UF8mKQFZXfqkGkXUSm34mLsdDKtSyk"}, {"kty": "RSA", "use": "sig", "kid": "SHEyYWcwNVk0LTdROTZzZ2FUWndIVXdack0xWUM5SEpwcS03dVUxWU4zRQ", "e": "AQAB", "n": "rRz52ddyP9Y2ezSlRsnkt-sjXfV_Ii7vOFX-cStLE3IUlVeSJGEe_kAASLr2r3BE2unjntaxj67NP8D95h_rzG1SpCklTEn-aTe3FOwNyTzUH_oiDVeRoEcf04Y43ciRGYRB5PhI6ii-2lYuig6hyUr776Qxiu6-0zw-M_ay2MgGSy5CEj55dDSvcUyxStUObxGpPWnEvybO1vnE7iJEWGNe0L5uPe5nLidOiR-JwjxSWEx1xZYtIjxaf2Ulu-qu4hwgwBUQdx4bNZyBfljKj55skWuHqPMG3xMjnedQC6Ms5bR3rIkbBpvmgI3kJK-4CZikM6ruyLo94-Lk19aYQw"}]} \ No newline at end of file diff --git a/tests/pub_iss.jwks b/tests/pub_iss.jwks index 77081f40..9b062907 100644 --- a/tests/pub_iss.jwks +++ b/tests/pub_iss.jwks @@ -1 +1 @@ -{"keys": [{"kty": "EC", "use": "sig", "kid": "SmdKMlVGcG1zMnprdDdXZGpGWEczdHhlZVpGbkx1THpPdUY4d0w4bnZkSQ", "crv": "P-256", "x": "tRHJYm0fsOi0icpGEb33qiDVgt68ltMoYSWdLGhDGz4", "y": "fRpX0i6p5Jigf5I0qwW34PyStosMShwWAWS8x_w5o7E"}, {"kty": "RSA", "use": "sig", "kid": "R0FsaFdqREFaUFp1c0MwbUpsbHVSZ200blBJZWJVMTUtNGsyVlBmdHk5UQ", "n": "2ilgsKVqF92KfhwmosSVeZOaDgb3RF1mbg-pqkmLO6YpOO06LF4V4angF-GhP-ysAm2E75aSIU4tnHVThFlcxTgKFqjYKJQXyVzTVK2r-L2IbvFPaDtvoU6WteybpMlIUVk2po3cFDGObCWYKCm7CUOLlwH0uOpui66P9VSCqdKVKbJRAQBvTSbP10KWPxulfqjWGJtHO5fY7-JVWwOBkG-eHSJIT_uaoPjyvKCZjknq04bLUV9qP78KRQpRyYijBN60w2v8F79baN9CN10TIEjjWKGz0uX0M_YYQzTUoSY5l5ka9RkL3wT4o2iQ1t5nHphX6aA-gqwgCQmi-nvjaw", "e": "AQAB"}]} \ No newline at end of file +{"keys": [{"kty": "EC", "use": "sig", "kid": "SmdKMlVGcG1zMnprdDdXZGpGWEczdHhlZVpGbkx1THpPdUY4d0w4bnZkSQ", "crv": "P-256", "x": "tRHJYm0fsOi0icpGEb33qiDVgt68ltMoYSWdLGhDGz4", "y": "fRpX0i6p5Jigf5I0qwW34PyStosMShwWAWS8x_w5o7E"}, {"kty": "RSA", "use": "sig", "kid": "R0FsaFdqREFaUFp1c0MwbUpsbHVSZ200blBJZWJVMTUtNGsyVlBmdHk5UQ", "e": "AQAB", "n": "2ilgsKVqF92KfhwmosSVeZOaDgb3RF1mbg-pqkmLO6YpOO06LF4V4angF-GhP-ysAm2E75aSIU4tnHVThFlcxTgKFqjYKJQXyVzTVK2r-L2IbvFPaDtvoU6WteybpMlIUVk2po3cFDGObCWYKCm7CUOLlwH0uOpui66P9VSCqdKVKbJRAQBvTSbP10KWPxulfqjWGJtHO5fY7-JVWwOBkG-eHSJIT_uaoPjyvKCZjknq04bLUV9qP78KRQpRyYijBN60w2v8F79baN9CN10TIEjjWKGz0uX0M_YYQzTUoSY5l5ka9RkL3wT4o2iQ1t5nHphX6aA-gqwgCQmi-nvjaw"}]} \ No newline at end of file From 1b7ae6827d62a130d4d805bb9ec29632cae6fc74 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 11 Sep 2023 20:36:10 +0200 Subject: [PATCH 03/39] Allow kwargs to flow through. Allow client_id to be assigned through a method argument. --- src/idpyoidc/client/client_auth.py | 4 +++- src/idpyoidc/client/oauth2/access_token.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/idpyoidc/client/client_auth.py b/src/idpyoidc/client/client_auth.py index 7e49969a..46740be6 100755 --- a/src/idpyoidc/client/client_auth.py +++ b/src/idpyoidc/client/client_auth.py @@ -501,9 +501,11 @@ def _construct_client_assertion(self, service, **kwargs): except KeyError: _args = {} + _client_id = kwargs.get("client_id", _entity.client_id) + # construct the signed JWT with the assertions and add # it as value to the 'client_assertion' claim of the request - return assertion_jwt(_entity.client_id, signing_key, audience, algorithm, **_args) + return assertion_jwt(_client_id, signing_key, audience, algorithm, **_args) def modify_request(self, request, service, **kwargs): """ diff --git a/src/idpyoidc/client/oauth2/access_token.py b/src/idpyoidc/client/oauth2/access_token.py index 0e2e1e62..6ced4897 100644 --- a/src/idpyoidc/client/oauth2/access_token.py +++ b/src/idpyoidc/client/oauth2/access_token.py @@ -34,8 +34,8 @@ class AccessToken(Service): "token_endpoint_auth_signing_alg": get_signing_algs, } - def __init__(self, upstream_get, conf=None): - Service.__init__(self, upstream_get, conf=conf) + def __init__(self, upstream_get, conf=None, **kwargs): + Service.__init__(self, upstream_get, conf=conf, **kwargs) self.pre_construct.append(self.oauth_pre_construct) def update_service_context(self, resp, key: Optional[str] = "", **kwargs): From 178f5756e967beaa4c32b3058717b6acd9f4bb23 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 16 Sep 2023 13:19:00 +0200 Subject: [PATCH 04/39] Remove having a default for always adding keys. --- src/idpyoidc/client/state_interface.py | 0 src/idpyoidc/node.py | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 src/idpyoidc/client/state_interface.py diff --git a/src/idpyoidc/client/state_interface.py b/src/idpyoidc/client/state_interface.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/idpyoidc/node.py b/src/idpyoidc/node.py index 915ac81f..655678b5 100644 --- a/src/idpyoidc/node.py +++ b/src/idpyoidc/node.py @@ -80,10 +80,10 @@ def make_keyjar( keyjar = KeyJar() keyjar.add_symmetric(client_id, _key) keyjar.add_symmetric("", _key) - else: - keyjar = build_keyjar(DEFAULT_KEY_DEFS) - if issuer_id: - keyjar.import_jwks(keyjar.export_jwks(private=True), issuer_id) + # else: + # keyjar = build_keyjar(DEFAULT_KEY_DEFS) + # if issuer_id: + # keyjar.import_jwks(keyjar.export_jwks(private=True), issuer_id) return keyjar From f8ff973018c7b9c1d4cf11c1c1f89fcd8be1c9bd Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 23 Sep 2023 08:34:42 +0200 Subject: [PATCH 05/39] Generalize token handling in client authentication Url used may have a query part which is not reflected in the htu parameter. Use DPoP access token as bearer token. Use latest cryptojwt. Fixed spelling error. --- setup.py | 2 +- src/idpyoidc/client/client_auth.py | 53 ++++++++++++++++++----- src/idpyoidc/client/service.py | 5 ++- src/idpyoidc/server/client_authn.py | 14 +++--- src/idpyoidc/server/oauth2/add_on/dpop.py | 13 ++---- src/idpyoidc/server/oidc/userinfo.py | 2 +- 6 files changed, 59 insertions(+), 30 deletions(-) diff --git a/setup.py b/setup.py index 3c2be62a..9c15c688 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ def run_tests(self): "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries :: Python Modules"], install_requires=[ - "cryptojwt>=1.8.1", + "cryptojwt>=1.8.3", "pyOpenSSL", "filelock>=3.0.12", 'pyyaml>=5.1.2', diff --git a/src/idpyoidc/client/client_auth.py b/src/idpyoidc/client/client_auth.py index 46740be6..49ddc3b3 100755 --- a/src/idpyoidc/client/client_auth.py +++ b/src/idpyoidc/client/client_auth.py @@ -12,6 +12,7 @@ from idpyoidc.defaults import DEF_SIGN_ALG from idpyoidc.defaults import JWT_BEARER +from idpyoidc.message import Message from idpyoidc.message.oauth2 import AccessTokenRequest from idpyoidc.message.oauth2 import SINGLE_OPTIONAL_STRING from idpyoidc.message.oidc import AuthnToken @@ -136,8 +137,8 @@ def _with_or_without_client_id(request, service): :param service: A :py:class:`idpyoidc.client.service.Service` instance """ if ( - isinstance(request, AccessTokenRequest) - and request["grant_type"] == "authorization_code" + isinstance(request, AccessTokenRequest) + and request["grant_type"] == "authorization_code" ): if "client_id" not in request: try: @@ -227,7 +228,7 @@ def modify_request(self, request, service, **kwargs): if not request["client_secret"]: raise AuthnFailure("Missing client secret") - # Set the client_id in the the request + # Set the client_id in the request request["client_id"] = _context.get_client_id() def construct(self, request, service=None, http_args=None, **kwargs): @@ -275,10 +276,38 @@ def find_token(request, token_type, service, **kwargs): except KeyError: # Get the latest acquired token. _state = kwargs.get("state", kwargs.get("key")) - _arg = service.upstream_get("context").cstate.get_set(_state, claim=[token_type]) + _arg = service.upstream_get("context").cstate.get_set(_state, claim=[token_type, + "token_type"]) return _arg.get("access_token") +def find_token_info(request: Union[Message, dict], token_type: str, service, **kwargs) -> dict: + """ + Token acquired by a previous run service. + + :param token_type: + :param kwargs: + :return: + """ + + if request is not None: + _token = request.get(token_type, None) + if _token: + del request[token_type] + # Required under certain circumstances :-) not under other + request.c_param[token_type] = SINGLE_OPTIONAL_STRING + return {token_type: _token, "token_type": "Bearer"} + + _token = kwargs.get("access_token", None) + if _token: + return {token_type: _token, "token_type": "Bearer"} + else: + # Get the latest acquired token. + _state = kwargs.get("state", kwargs.get("key")) + return service.upstream_get("context").cstate.get_set(_state, claim=[token_type, + "token_type"]) + + class BearerHeader(ClientAuthnMethod): """The bearer header authentication method.""" @@ -295,18 +324,20 @@ def construct(self, request=None, service=None, http_args=None, **kwargs): """ if service.service_name == "refresh_token": - _acc_token = find_token(request, "refresh_token", service, **kwargs) + _token_type = "refresh_token" elif service.service_name == "token_exchange": - _acc_token = find_token(request, "subject_token", service, **kwargs) + _token_type = "subject_token" else: - _acc_token = find_token(request, "access_token", service, **kwargs) + _token_type = "access_token" - if not _acc_token: + _token_info = find_token_info(request, _token_type, service, **kwargs) + + if not _token_info: raise KeyError("No bearer token available") - # The authorization value starts with 'Bearer' when bearer tokens - # are used - _bearer = "Bearer {}".format(_acc_token) + # The authorization value starts with the token_type + #if _token_info["token_type"].to_lower() != "bearer": + _bearer = f"{_token_info['token_type']} {_token_info[_token_type]}" # Add 'Authorization' to the headers if http_args is None: diff --git a/src/idpyoidc/client/service.py b/src/idpyoidc/client/service.py index aeb23619..0bf48e13 100644 --- a/src/idpyoidc/client/service.py +++ b/src/idpyoidc/client/service.py @@ -383,8 +383,9 @@ def get_headers( ) _authz = _headers.get("Authorization") - if _authz and _authz.startswith("Bearer"): - kwargs["token"] = _authz.split(" ")[1] + if _authz: + if _authz.startswith("Bearer") or _authz.startswith("DPoP"): + kwargs["token"] = _authz.split(" ")[1] for meth in self.construct_extra_headers: _headers = meth( diff --git a/src/idpyoidc/server/client_authn.py b/src/idpyoidc/server/client_authn.py index 119b80f4..5fdbb2f9 100755 --- a/src/idpyoidc/server/client_authn.py +++ b/src/idpyoidc/server/client_authn.py @@ -227,12 +227,14 @@ def _verify( ): token = authorization_token.split(" ", 1)[1] _context = self.upstream_get("context") - try: - client_id = get_client_id_from_token(_context, token, request) - except ToOld: - raise BearerTokenAuthenticationError("Expired token") - except KeyError: - raise BearerTokenAuthenticationError("Unknown token") + client_id = "" + if get_client_id_from_token: + try: + client_id = get_client_id_from_token(_context, token, request) + except ToOld: + raise BearerTokenAuthenticationError("Expired token") + except KeyError: + raise BearerTokenAuthenticationError("Unknown token") return {"token": token, "client_id": client_id} diff --git a/src/idpyoidc/server/oauth2/add_on/dpop.py b/src/idpyoidc/server/oauth2/add_on/dpop.py index c6c7a29f..8bb84471 100644 --- a/src/idpyoidc/server/oauth2/add_on/dpop.py +++ b/src/idpyoidc/server/oauth2/add_on/dpop.py @@ -147,7 +147,7 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs # The signature of the JWS is verified, now for checking the # content - if _dpop["htu"] != _http_info["url"]: + if _dpop["htu"] != _http_info["url"].split('?')[0]: raise ValueError("htu in DPoP does not match the HTTP URI") if _dpop["htm"] != _http_info["method"]: @@ -209,8 +209,8 @@ def add_support(endpoint: dict, **kwargs): class DPoPClientAuth(BearerHeader): tag = "dpop_client_auth" - def is_usable(self, request=None, authorization_info=None, http_headers=None): - if authorization_info is not None and authorization_info.startswith("DPoP "): + def is_usable(self, request=None, authorization_token=None, http_headers=None): + if authorization_token is not None and authorization_token.startswith("DPoP "): return True return False @@ -226,9 +226,4 @@ def verify( info = BearerHeader._verify( self, request, authorization_token, endpoint, get_client_id_from_token, **kwargs ) - _context = self.upstream_get("context") - return {"client_id": ""} - # if _context.cdb[client_info["id"]]["client_secret"] == client_info["secret"]: - # return {"client_id": client_info["id"]} - # else: - # raise ClientAuthenticationError() + return info diff --git a/src/idpyoidc/server/oidc/userinfo.py b/src/idpyoidc/server/oidc/userinfo.py index 2d8f624b..49de84d4 100755 --- a/src/idpyoidc/server/oidc/userinfo.py +++ b/src/idpyoidc/server/oidc/userinfo.py @@ -147,7 +147,7 @@ def process_request(self, request=None, **kwargs): ) allowed = False - # This has to be made more fine grained. + # This has to be made more finegrained. # if "offline_access" in session["authn_req"]["scope"]: # pass From 32fc941bc242f17e8809e66f393299eb275c3c39 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 25 Sep 2023 09:45:34 +0200 Subject: [PATCH 06/39] get_jwks was undefined. --- src/idpyoidc/claims.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idpyoidc/claims.py b/src/idpyoidc/claims.py index e174f62b..c4b01335 100644 --- a/src/idpyoidc/claims.py +++ b/src/idpyoidc/claims.py @@ -132,7 +132,7 @@ def add_extra_keys(self, keyjar, id): return None def get_jwks(self, keyjar): - return None + return keyjar.export_jwks() def handle_keys(self, configuration: dict, keyjar: Optional[KeyJar] = None): _jwks = _jwks_uri = None From 6bda02528f36769261576c736cba9b6ba7949add Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Thu, 12 Oct 2023 10:22:19 +0200 Subject: [PATCH 07/39] Fixed a bug in the dump/load system --- src/idpyoidc/__init__.py | 2 +- src/idpyoidc/claims.py | 2 +- src/idpyoidc/impexp.py | 61 ++++++++--- src/idpyoidc/node.py | 4 +- src/idpyoidc/server/session/database.py | 6 +- src/idpyoidc/server/token/handler.py | 13 +-- src/idpyoidc/util.py | 13 ++- tests/test_server_50_persistence.py | 132 ++++++++++++++++++++---- 8 files changed, 184 insertions(+), 49 deletions(-) diff --git a/src/idpyoidc/__init__.py b/src/idpyoidc/__init__.py index 51dca059..c7e69bd5 100644 --- a/src/idpyoidc/__init__.py +++ b/src/idpyoidc/__init__.py @@ -1,5 +1,5 @@ __author__ = "Roland Hedberg" -__version__ = "2.2.0" +__version__ = "3.0.0" VERIFIED_CLAIM_PREFIX = "__verified" diff --git a/src/idpyoidc/claims.py b/src/idpyoidc/claims.py index e174f62b..c4b01335 100644 --- a/src/idpyoidc/claims.py +++ b/src/idpyoidc/claims.py @@ -132,7 +132,7 @@ def add_extra_keys(self, keyjar, id): return None def get_jwks(self, keyjar): - return None + return keyjar.export_jwks() def handle_keys(self, configuration: dict, keyjar: Optional[KeyJar] = None): _jwks = _jwks_uri = None diff --git a/src/idpyoidc/impexp.py b/src/idpyoidc/impexp.py index 94592c0f..ff8938ea 100644 --- a/src/idpyoidc/impexp.py +++ b/src/idpyoidc/impexp.py @@ -1,12 +1,12 @@ +import base64 from typing import Any from typing import List from typing import Optional -from cryptojwt import as_unicode -from cryptojwt.utils import as_bytes from cryptojwt.utils import importer from cryptojwt.utils import qualified_name +# from idpyoidc.item import DLDict from idpyoidc.message import Message from idpyoidc.storage import DictType @@ -15,6 +15,23 @@ def fully_qualified_name(cls): return cls.__module__ + "." + cls.__class__.__name__ +def type2cls(v): + if isinstance(v, str): + return "" + elif isinstance(v, int): + return 0 + elif isinstance(v, bool): + return bool + elif isinstance(v, bytes): + return b"" + elif isinstance(v, dict): + return {} + elif isinstance(v, list): + return [] + else: + return None + + class ImpExp: parameter = {} special_load_dump = {} @@ -24,11 +41,14 @@ def __init__(self): pass def dump_attr(self, cls, item, exclude_attributes: Optional[List[str]] = None) -> dict: - if cls in [None, 0, "", [], {}, bool, b""]: - if cls == b"": - val = as_unicode(item) - else: - val = item + if cls in [None, 0, "", bool]: + val = item + elif cls == b"": + val = f"BYTES:{base64.b64encode(item).decode('utf-8')}" + elif cls == {} and isinstance(item, dict): + val = {k: self.dump_attr(type2cls(v), v, exclude_attributes) for k, v in item.items()} + elif cls == [] and isinstance(item, list): + val = [self.dump_attr(type2cls(v), v, exclude_attributes) for v in item] elif cls == "DICT_TYPE": if isinstance(item, dict): val = item @@ -78,11 +98,11 @@ def local_load_adjustments(self, **kwargs): pass def load_attr( - self, - cls: Any, - item: dict, - init_args: Optional[dict] = None, - load_args: Optional[dict] = None, + self, + cls: Any, + item: Any, + init_args: Optional[dict] = None, + load_args: Optional[dict] = None, ) -> Any: if load_args: _kwargs = {"load_args": load_args} @@ -92,11 +112,17 @@ def load_attr( if init_args: _kwargs["init_args"] = init_args - if cls in [None, 0, "", [], {}, bool, b""]: - if cls == b"": - val = as_bytes(item) + if cls in [None, 0, "", bool]: + if cls == "" and item.startswith("BYTES:"): + val = base64.b64decode(item[len("BYTES:"):].encode("utf-8")) else: val = item + elif cls == b"": + val = base64.b64decode(item[len("BYTES:"):].encode("utf-8")) + elif cls == {}: + val = {k: self.load_attr(type2cls(v), v, init_args, load_args) for k, v in item.items()} + elif cls == []: + val = [self.load_attr(type2cls(v), v, init_args, load_args) for v in item] elif cls == "DICT_TYPE": if list(item.keys()) == ["DICT_TYPE"]: _spec = item["DICT_TYPE"] @@ -127,7 +153,10 @@ def load_attr( else: _args = {} - val = cls(**_args).load(item, **_kwargs) + if item: + val = cls(**_args).load(item, **_kwargs) + else: + val = cls(**_args) return val diff --git a/src/idpyoidc/node.py b/src/idpyoidc/node.py index 655678b5..448272d6 100644 --- a/src/idpyoidc/node.py +++ b/src/idpyoidc/node.py @@ -3,10 +3,8 @@ from typing import Union from cryptojwt import KeyJar -from cryptojwt.key_jar import build_keyjar from cryptojwt.key_jar import init_key_jar -from idpyoidc.client.defaults import DEFAULT_KEY_DEFS from idpyoidc.configure import Configuration from idpyoidc.impexp import ImpExp from idpyoidc.util import instantiate @@ -51,7 +49,7 @@ def make_keyjar( key_conf: Optional[dict] = None, issuer_id: Optional[str] = "", client_id: Optional[str] = "", - ): +): if keyjar is False: return None diff --git a/src/idpyoidc/server/session/database.py b/src/idpyoidc/server/session/database.py index 1a8191ff..bc83d744 100644 --- a/src/idpyoidc/server/session/database.py +++ b/src/idpyoidc/server/session/database.py @@ -17,6 +17,7 @@ from idpyoidc.util import rndstr from .grant import Grant from .info import NodeInfo +from ...util import instantiate logger = logging.getLogger(__name__) @@ -183,5 +184,6 @@ def flush(self): self.db = DLDict() def local_load_adjustments(self, **kwargs): - _crypt = init_encrypter(self.crypt_config) - self.crypt = _crypt["encrypter"] + self.crypt = instantiate(self.crypt_config["class"], **self.crypt_config["kwargs"]) + # _crypt = init_encrypter(self.crypt_config) + # self.crypt = _crypt["encrypter"] diff --git a/src/idpyoidc/server/token/handler.py b/src/idpyoidc/server/token/handler.py index ad2ae4e9..b37f5eb1 100755 --- a/src/idpyoidc/server/token/handler.py +++ b/src/idpyoidc/server/token/handler.py @@ -147,6 +147,7 @@ def factory( refresh: Optional[dict] = None, id_token: Optional[dict] = None, jwks_file: Optional[str] = "", + jwks_def: Optional[dict] = None, **kwargs ) -> TokenHandler: """ @@ -166,15 +167,15 @@ def factory( "idtoken": "id_token", } - key_defs = [] read_only = False cwd = upstream_get("attribute", "cwd") - if kwargs.get("jwks_def"): - defs = kwargs["jwks_def"] + if jwks_def: if not jwks_file: - jwks_file = defs.get("private_path", os.path.join(cwd, JWKS_FILE)) - read_only = defs.get("read_only", read_only) - key_defs = defs.get("key_defs", []) + jwks_file = jwks_def.get("private_path", os.path.join(cwd, JWKS_FILE)) + read_only = jwks_def.get("read_only", read_only) + key_defs = jwks_def.get("key_defs", []) + else: + key_defs = None # if not jwks_file: # jwks_file = os.path.join(cwd, JWKS_FILE) diff --git a/src/idpyoidc/util.py b/src/idpyoidc/util.py index 21c66afa..0d8d3424 100644 --- a/src/idpyoidc/util.py +++ b/src/idpyoidc/util.py @@ -1,3 +1,4 @@ +import base64 import importlib import json import logging @@ -11,8 +12,8 @@ from urllib.parse import urlsplit from urllib.parse import urlunsplit -import yaml from cryptojwt.utils import importer +import yaml logger = logging.getLogger(__name__) @@ -107,6 +108,16 @@ def deserialize(self, str): return str +class Base64(object): + @staticmethod + def serialize(str): + return base64.b64encode(str.encode("utf-8")).decode("utf-8") + + @staticmethod + def deserialize(str): + return base64.b64decode(str.encode("utf-8")).decode("utf-8") + + def get_http_params(config): params = config.get("httpc_params", {}) diff --git a/tests/test_server_50_persistence.py b/tests/test_server_50_persistence.py index 7cea5afc..bdb6c408 100644 --- a/tests/test_server_50_persistence.py +++ b/tests/test_server_50_persistence.py @@ -66,6 +66,22 @@ client_secret="hemligt", ) +AUTH_REQ_2 = AuthorizationRequest( + client_id="client_2", + redirect_uri="https://two.example.com/cb", + scope=["openid"], + state="STATE", + response_type="code", +) + +TOKEN_REQ_2 = AccessTokenRequest( + client_id="client_2", + redirect_uri="https://two.example.com/cb", + state="STATE", + grant_type="authorization_code", + client_secret="hemligt", +) + TOKEN_REQ_DICT = TOKEN_REQ.to_dict() BASEDIR = os.path.abspath(os.path.dirname(__file__)) @@ -221,21 +237,39 @@ def create_endpoint(self): ) # The top most part (Server class instance) is not - server1.context.cdb["client_1"] = { - "client_secret": "hemligt", - "redirect_uris": [("https://example.com/cb", None)], - "client_salt": "salted", - "token_endpoint_auth_method": "client_secret_post", - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": [ - "openid", - "profile", - "email", - "address", - "phone", - "offline_access", - "research_and_scholarship", - ], + server1.context.cdb = { + "client_1": { + "client_secret": "hemligt", + "redirect_uris": [("https://example.com/cb", None)], + "client_salt": "salted", + "token_endpoint_auth_method": "client_secret_post", + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": [ + "openid", + "profile", + "email", + "address", + "phone", + "offline_access", + "research_and_scholarship", + ], + }, + "client_2": { + "client_secret": "hemligt_ord", + "redirect_uris": [("https://two.example.com/cb", None)], + "client_salt": "salted peanuts", + "token_endpoint_auth_method": "client_secret_post", + "response_types": ["code", "code id_token", "id_token"], + "allowed_scopes": [ + "openid", + "profile", + "email", + "address", + "phone", + "offline_access", + "research_and_scholarship", + ] + } } # make server2 endpoint context a copy of server 1 endpoint context @@ -315,8 +349,9 @@ def test_init(self): "openid" } assert ( - self.endpoint[1].upstream_get("context").provider_info["claims_parameter_supported"] - == self.endpoint[2].upstream_get("context").provider_info["claims_parameter_supported"] + self.endpoint[1].upstream_get("context").provider_info["claims_parameter_supported"] + == self.endpoint[2].upstream_get("context").provider_info[ + "claims_parameter_supported"] ) def test_parse(self): @@ -474,7 +509,7 @@ def test_sman_db_integrity(self): sman = self.session_manager[1] session_dump = sman.dump() - # there after an exception a database could be inconsistent + # after an exception a database could be inconsistent # it would be better to always flush database when a new http request come # and load session from previously loaded sessions sman.flush() @@ -499,7 +534,66 @@ def test_sman_db_integrity(self): # some mess before doing that sman.crypt_config = {"password": "ingoalla", "salt": "fantozzi"} - # ok, end of the games, session have been loaded and all the things be finally there! + # ok, end of the game, session have been loaded and all the things should finally be there! sman.load(session_dump) for i in "db", "crypt_config": assert session_dump[i] == sman.dump()[i] + + def _get_client_session_info(self, client_id, db): + res = {} + for key, info in db.items(): + val = self.session_manager[1].unpack_branch_key(key) + if len(val) > 1 and val[1] == client_id: + res[key] = info + if val[0] not in res: + res[val[0]] = db[val[0]] + + return res + + def test_multiple_sessions(self): + session_id = self._create_session(AUTH_REQ, index=1) + grant = self.endpoint[1].upstream_get("context").authz(session_id, AUTH_REQ) + code = self._mint_code(grant, session_id, index=1) + access_token_1 = self._mint_access_token(grant, session_id, code, 1) + + session_id = self._create_session(AUTH_REQ_2, index=1) + grant = self.endpoint[1].upstream_get("context").authz(session_id, AUTH_REQ_2) + code = self._mint_code(grant, session_id, index=1) + access_token_2 = self._mint_access_token(grant, session_id, code, 1) + + _session_state = self.session_manager[1].dump() + _orig_db = _session_state["db"] + _client_1_db = self._get_client_session_info('client_1', _orig_db) + _session_state["db"] = _client_1_db + + self.session_manager[2].load( + _session_state, init_args={"upstream_get": self.endpoint[2].upstream_get} + ) + + http_info = {"headers": {"authorization": "Bearer {}".format(access_token_1.value)}} + _req = self.endpoint[2].parse_request({}, http_info=http_info) + args = self.endpoint[2].process_request(_req) + assert args["client_id"] == "client_1" + + # this should not work + + http_info = {"headers": {"authorization": "Bearer {}".format(access_token_2.value)}} + _req = self.endpoint[2].parse_request({}, http_info=http_info) + + assert _req["error"] == "invalid_token" + + _token_info = self.session_manager[1].token_handler.info(access_token_2.value) + sid = _token_info.get("sid") + _path = self.session_manager[1].decrypt_branch_id(sid) + + _client_db = self._get_client_session_info(_path[1], _orig_db) + _session_state["db"] = _client_db + + self.session_manager[2].load( + _session_state, init_args={"upstream_get": self.endpoint[2].upstream_get} + ) + + http_info = {"headers": {"authorization": "Bearer {}".format(access_token_2.value)}} + _req = self.endpoint[2].parse_request({}, http_info=http_info) + args = self.endpoint[2].process_request(_req) + assert args["client_id"] == "client_2" From 16d7da0ce760fa315f7f21a7acf3220ba9e0af68 Mon Sep 17 00:00:00 2001 From: roland Date: Fri, 13 Oct 2023 08:51:46 +0200 Subject: [PATCH 08/39] Fixed bug in AbstractFileSystem delete method. --- src/idpyoidc/storage/abfile.py | 42 +++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/idpyoidc/storage/abfile.py b/src/idpyoidc/storage/abfile.py index cb80182d..6257fe21 100644 --- a/src/idpyoidc/storage/abfile.py +++ b/src/idpyoidc/storage/abfile.py @@ -24,11 +24,11 @@ class AbstractFileSystem(DictType): """ def __init__( - self, - fdir: Optional[str] = "", - key_conv: Optional[str] = "", - value_conv: Optional[str] = "", - **kwargs + self, + fdir: Optional[str] = "", + key_conv: Optional[str] = "", + value_conv: Optional[str] = "", + **kwargs ): """ items = FileSystem( @@ -86,11 +86,12 @@ def __getitem__(self, item): item = self.key_conv.serialize(item) if self.is_changed(item): - logger.info("File content change in {}".format(item)) + logger.info(f"File content change in {item}") fname = os.path.join(self.fdir, item) self.storage[item] = self._read_info(fname) - logger.debug('Read from "%s"', item) + _msg = f'Read from "{item}"' + logger.debug(_msg) # storage values are already value converted return self.storage[item] @@ -114,21 +115,27 @@ def __setitem__(self, key, value): _key = key fname = os.path.join(self.fdir, _key) - lock = FileLock("{}.lock".format(fname)) + lock = FileLock(f"{fname}.lock") with lock: with open(fname, "w") as fp: fp.write(self.value_conv.serialize(value)) self.storage[_key] = value - logger.debug('Wrote to "%s"', key) + _msg = f'Wrote to "{key}"' + logger.debug(_msg) self.fmtime[_key] = self.get_mtime(fname) def __delitem__(self, key): fname = os.path.join(self.fdir, key) - if os.path.isfile(fname): - lock = FileLock("{}.lock".format(fname)) - with lock: + if fname.endswith(".lock"): + if os.path.isfile(fname): os.unlink(fname) + else: + if os.path.isfile(fname): + lock = FileLock(f"{fname}.lock") + with lock: + os.unlink(fname) + os.unlink(f"{fname}.lock") try: del self.storage[key] @@ -190,15 +197,17 @@ def is_changed(self, item): def _read_info(self, fname): if os.path.isfile(fname): try: - lock = FileLock("{}.lock".format(fname)) + lock = FileLock(f"{fname}.lock") with lock: info = open(fname, "r").read().strip() + lock.release() return self.value_conv.deserialize(info) except Exception as err: logger.error(err) raise else: - logger.error("No such file: {}".format(fname)) + _msg = f"No such file: '{fname}'" + logger.error(_msg) return None def synch(self): @@ -225,7 +234,8 @@ def synch(self): try: self.storage[f] = self._read_info(fname) except Exception as err: - logger.warning("Bad content in {} ({})".format(fname, err)) + _msg = f"Bad content in {fname} ({err})" + logger.warning(_msg) else: self.fmtime[f] = mtime @@ -287,7 +297,7 @@ def __len__(self): return n def __str__(self): - return "{config:" + str(self.config) + ", info:" + str(self.storage) + "}" + return f"info:{self.storage}" def dump(self): return {k: v for k, v in self.items()} From 5bd10210a89609e8ea5a0525471abc95fcf20c5c Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 19 Oct 2023 10:01:25 +0200 Subject: [PATCH 09/39] Added more logging and fixed bugs in flask_rp/ciews.py --- example/flask_op/config.json | 9 +---- example/flask_op/private/cookie_jwks.json | 2 +- example/flask_rp/config.json | 38 +++++++++---------- example/flask_rp/views.py | 13 ++----- src/idpyoidc/client/entity.py | 2 +- src/idpyoidc/client/oauth2/__init__.py | 3 ++ src/idpyoidc/client/oauth2/server_metadata.py | 12 ++++++ src/idpyoidc/client/rp_handler.py | 1 + src/idpyoidc/client/service.py | 12 ++++++ src/idpyoidc/server/oidc/registration.py | 21 +++++----- 10 files changed, 67 insertions(+), 46 deletions(-) diff --git a/example/flask_op/config.json b/example/flask_op/config.json index 19432ffb..2fe378fc 100644 --- a/example/flask_op/config.json +++ b/example/flask_op/config.json @@ -39,7 +39,7 @@ "server_info": { "add_on": { "pkce": { - "function": "idpyoidc.server.oidc.add_on.pkce.add_pkce_support", + "function": "idpyoidc.server.oauth2.add_on.pkce.add_support", "kwargs": { "essential": false, "code_challenge_method": "S256 S384 S512" @@ -208,13 +208,8 @@ "request_uri_parameter_supported": true, "response_types_supported": [ "code", - "token", "id_token", - "code token", - "code id_token", - "id_token token", - "code id_token token", - "none" + "code id_token" ], "response_modes_supported": [ "query", diff --git a/example/flask_op/private/cookie_jwks.json b/example/flask_op/private/cookie_jwks.json index 3aafdf12..1526761c 100644 --- a/example/flask_op/private/cookie_jwks.json +++ b/example/flask_op/private/cookie_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "gVdGvOn9TFG38gzqs2uO-pQB40qxJbfm"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "HlW_dFC6aquKPZ6zGtq2dZEBcntP9uHd"}]} \ No newline at end of file +{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "LrU7gu0Jcj_3XJ0cPeUuxA0-jq5H792-"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "FQguegtRW6c0fXxDhke8dIg9QDddiAYX"}]} \ No newline at end of file diff --git a/example/flask_rp/config.json b/example/flask_rp/config.json index ba980896..8ffdff51 100644 --- a/example/flask_rp/config.json +++ b/example/flask_rp/config.json @@ -87,26 +87,26 @@ }, "clients": { "": { - "client_preferences": { + "httpc_params": { + "verify": false + }, + "client_type": "oidc", + "capabilities": { "application_name": "rphandler", - "metadata": { - "application_type": "web", - "contacts": [ - "ops@example.com" - ], - "response_types": [ - "code" - ] - }, - "usage": { - "scope": [ - "openid", - "profile", - "email", - "address", - "phone" - ] - }, + "application_type": "web", + "contacts": [ + "ops@example.com" + ], + "response_types_supported": [ + "code" + ], + "scopes_supported": [ + "openid", + "profile", + "email", + "address", + "phone" + ], "token_endpoint_auth_methods": [ "client_secret_basic", "client_secret_post" diff --git a/example/flask_rp/views.py b/example/flask_rp/views.py index d833a2d2..e74665d0 100644 --- a/example/flask_rp/views.py +++ b/example/flask_rp/views.py @@ -67,7 +67,7 @@ def rp(): except Exception as err: return make_response('Something went wrong:{}'.format(err), 400) else: - response = redirect(result['url'], 303) + response = redirect(result, 303) return response else: _providers = current_app.rp_config.clients.keys() @@ -106,7 +106,7 @@ def finalize(op_identifier, request_args): session['state'] = request_args.get('state') if session['state']: - iss = _context.state.get_iss(session['state']) + iss = _context.cstate.get_set(session['state'], claim=["iss"])['iss'] else: return make_response('Unknown state', 400) @@ -158,14 +158,9 @@ def get_op_identifier_by_cb_uri(url: str): uri = splitquery(url)[0] for k, v in current_app.rph.issuer2rp.items(): for endpoint in v.get_callback_uris(): - _endps = v.get_metadata_value(endpoint) - if _endps is None: - continue - elif isinstance(_endps,str): - if _endps == uri: + for val in v.context.claims.get_preference(endpoint): + if val == uri: return k - elif uri in _endps: - return k return None @oidc_rp_views.route('/authz_cb/') diff --git a/src/idpyoidc/client/entity.py b/src/idpyoidc/client/entity.py index 22b5df2f..4b446685 100644 --- a/src/idpyoidc/client/entity.py +++ b/src/idpyoidc/client/entity.py @@ -216,4 +216,4 @@ def import_keys(self, keyspec): return _keyjar def get_callback_uris(self): - return self.context.claims.callback_uri + return self.context.claims.get_preference("callback_uris") diff --git a/src/idpyoidc/client/oauth2/__init__.py b/src/idpyoidc/client/oauth2/__init__.py index 51eb5a16..d28423ef 100755 --- a/src/idpyoidc/client/oauth2/__init__.py +++ b/src/idpyoidc/client/oauth2/__init__.py @@ -168,6 +168,8 @@ def get_response( if 300 <= resp.status_code < 400: return {"http_response": resp} + elif resp.status_code >= 400: + logger.error(f"HTTP error: {resp}") if resp.status_code < 300: if "keyjar" not in kwargs: @@ -333,6 +335,7 @@ def dynamic_provider_info_discovery(client: Client, behaviour_args: Optional[dic except KeyError: pass + logger.debug(f"{service}") response = client.do_request(service, behaviour_args=behaviour_args) if is_error_message(response): raise OidcServiceError(response["error"]) diff --git a/src/idpyoidc/client/oauth2/server_metadata.py b/src/idpyoidc/client/oauth2/server_metadata.py index 8fe4ecc9..bf6bbb2a 100644 --- a/src/idpyoidc/client/oauth2/server_metadata.py +++ b/src/idpyoidc/client/oauth2/server_metadata.py @@ -121,17 +121,29 @@ def _update_service_context(self, resp): try: _keyjar = self.upstream_get("attribute", "keyjar") if _keyjar is None: + LOGGER.debug("No existing KeyJar") _keyjar = KeyJar() except KeyError: _keyjar = KeyJar() + _hp = self.upstream_get('entity').httpc_params + if "verify" in _hp and "verify" not in _keyjar.httpc_params: + _keyjar.httpc_params["verify"] = _hp["verify"] + # Load the keys. Note that this only means that the key specification # is loaded not necessarily that any keys are fetched. if "jwks_uri" in resp: + LOGGER.debug(f"'jwks_uri' in provider info: {resp['jwks_uri']}") _keyjar.load_keys(_pcr_issuer, jwks_uri=resp["jwks_uri"]) elif "jwks" in resp: + LOGGER.debug("'jwks' in provider info") _keyjar.load_keys(_pcr_issuer, jwks=resp["jwks"]) + else: + LOGGER.debug("Neither jws or jwks_uri in provider info") + LOGGER.debug(f"loaded keys for: {_pcr_issuer}") + LOGGER.debug(f"keys = {_keyjar.key_summary(_pcr_issuer)}") + LOGGER.debug(f"{_keyjar}") # Combine what I prefer/supports with what the Provider supports if isinstance(resp, Message): _info = resp.to_dict() diff --git a/src/idpyoidc/client/rp_handler.py b/src/idpyoidc/client/rp_handler.py index fe44054d..b6b730bd 100644 --- a/src/idpyoidc/client/rp_handler.py +++ b/src/idpyoidc/client/rp_handler.py @@ -186,6 +186,7 @@ def init_client(self, issuer): if self.jwks_uri: _cnf["jwks_uri"] = self.jwks_uri + logger.debug(f"config: {_cnf}") try: client = self.client_cls( services=_services, diff --git a/src/idpyoidc/client/service.py b/src/idpyoidc/client/service.py index 0bf48e13..b7a310c5 100644 --- a/src/idpyoidc/client/service.py +++ b/src/idpyoidc/client/service.py @@ -8,9 +8,11 @@ from typing import Union from urllib.parse import urlparse +from cryptojwt.exception import IssuerNotFound from cryptojwt.jwe.jwe import factory as jwe_factory from cryptojwt.jws.jws import factory as jws_factory from cryptojwt.jwt import JWT +from idpyoidc.exception import MissingSigningKey from idpyoidc.client.exception import Unsupported from idpyoidc.impexp import ImpExp @@ -649,6 +651,16 @@ def parse_response( try: # verify the message. If something is wrong an exception is thrown resp.verify(**vargs) + except MissingSigningKey as err: + LOGGER.error(f"Could not find an appropriate key: {err}") + _keyjar = self.upstream_get("attribute", "keyjar") + try: + LOGGER.debug(f"[{self.upstream_get('entity').client_id}] Available keys for" + f" {vargs['iss']}:" + f" {_keyjar.key_summary(vargs['iss'])}") + except IssuerNotFound: + LOGGER.debug(f"Issuer not found in keyjar: {vargs['iss']}") + raise except Exception as err: LOGGER.error("Got exception while verifying response: %s", err) raise diff --git a/src/idpyoidc/server/oidc/registration.py b/src/idpyoidc/server/oidc/registration.py index e7d34927..1b8f0bed 100644 --- a/src/idpyoidc/server/oidc/registration.py +++ b/src/idpyoidc/server/oidc/registration.py @@ -4,6 +4,7 @@ import logging import secrets from typing import List +from typing import Optional from urllib.parse import urlencode from urllib.parse import urlparse @@ -283,12 +284,7 @@ def do_client_registration(self, request, client_id, ignore=None): # if it can't load keys because the URL is false it will # just silently fail. Waiting for better times. _keyjar.load_keys(client_id, jwks_uri=t["jwks_uri"], jwks=t["jwks"]) - - n_keys = 0 - for kb in _keyjar.get(client_id, []): - n_keys += len(kb.keys()) - msg = "found {} keys for client_id={}" - logger.debug(msg.format(n_keys, client_id)) + logger.debug(f"Keys for {client_id}: {_keyjar.key_summary(client_id)}") return _cinfo @@ -403,7 +399,10 @@ def add_client_secret(self, cinfo, client_id, context): return client_secret - def client_registration_setup(self, request, new_id=True, set_secret=True): + def client_registration_setup(self, request, + new_id: Optional[bool] = True, + set_secret: Optional[bool] = True, + reserved_client_id: Optional[list] = None): try: request.verify() except (MessageException, ValueError) as err: @@ -433,7 +432,9 @@ def client_registration_setup(self, request, new_id=True, set_secret=True): else: cid_generator = importer("idpyoidc.server.oidc.registration.random_client_id") cid_gen_kwargs = {} - client_id = cid_generator(reserved=_context.cdb.keys(), **cid_gen_kwargs) + if not reserved_client_id: + reserved_client_id = _context.cdb.keys() + client_id = cid_generator(reserved=reserved_client_id, **cid_gen_kwargs) if "client_id" in request: del request["client_id"] else: @@ -488,7 +489,9 @@ def client_registration_setup(self, request, new_id=True, set_secret=True): def process_request(self, request=None, new_id=True, set_secret=True, **kwargs): try: - reg_resp = self.client_registration_setup(request, new_id, set_secret) + reserved_client_id = kwargs.get("reserved") + reg_resp = self.client_registration_setup(request, new_id, set_secret, + reserved_client_id) except Exception as err: logger.error("client_registration_setup: %s", request) return ResponseMessage( From 604e3b155886b6c943249874984e71f5f98c3a0b Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 28 Oct 2023 08:37:09 +0200 Subject: [PATCH 10/39] Need the httpc_params when doing a http call. --- src/idpyoidc/client/oauth2/server_metadata.py | 20 ++++++++++++------- src/idpyoidc/server/util.py | 13 +++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/idpyoidc/client/oauth2/server_metadata.py b/src/idpyoidc/client/oauth2/server_metadata.py index bf6bbb2a..370256d5 100644 --- a/src/idpyoidc/client/oauth2/server_metadata.py +++ b/src/idpyoidc/client/oauth2/server_metadata.py @@ -126,24 +126,30 @@ def _update_service_context(self, resp): except KeyError: _keyjar = KeyJar() - _hp = self.upstream_get('entity').httpc_params - if "verify" in _hp and "verify" not in _keyjar.httpc_params: - _keyjar.httpc_params["verify"] = _hp["verify"] - + _loaded = False # Load the keys. Note that this only means that the key specification # is loaded not necessarily that any keys are fetched. if "jwks_uri" in resp: LOGGER.debug(f"'jwks_uri' in provider info: {resp['jwks_uri']}") + _hp = self.upstream_get('entity').httpc_params + if "verify" in _hp and "verify" not in _keyjar.httpc_params: + _keyjar.httpc_params["verify"] = _hp["verify"] _keyjar.load_keys(_pcr_issuer, jwks_uri=resp["jwks_uri"]) + _loaded = True elif "jwks" in resp: LOGGER.debug("'jwks' in provider info") _keyjar.load_keys(_pcr_issuer, jwks=resp["jwks"]) + _loaded = True else: LOGGER.debug("Neither jws or jwks_uri in provider info") - LOGGER.debug(f"loaded keys for: {_pcr_issuer}") - LOGGER.debug(f"keys = {_keyjar.key_summary(_pcr_issuer)}") - LOGGER.debug(f"{_keyjar}") + if _loaded: + LOGGER.debug(f"loaded keys for: {_pcr_issuer}") + LOGGER.debug(f"keys = {_keyjar.key_summary(_pcr_issuer)}") + LOGGER.debug(f"{_keyjar}") + else: + LOGGER.debug(f"Did not load any keys for {_pcr_issuer}") + # Combine what I prefer/supports with what the Provider supports if isinstance(resp, Message): _info = resp.to_dict() diff --git a/src/idpyoidc/server/util.py b/src/idpyoidc/server/util.py index 1105da88..55f409c7 100755 --- a/src/idpyoidc/server/util.py +++ b/src/idpyoidc/server/util.py @@ -42,14 +42,11 @@ def build_endpoints(conf, upstream_get, issuer): else: _instance = spec["class"](upstream_get=upstream_get, **kwargs) - try: - _path = spec["path"] - except KeyError: - # Should there be a default ? - raise - - _instance.endpoint_path = _path - _instance.full_path = "{}/{}".format(_url, _path) + _path = spec.get("path", "") + + if _path: + _instance.endpoint_path = _path + _instance.full_path = "{}/{}".format(_url, _path) endpoint[_instance.name] = _instance From ceba7be80b1428a4166f6ac7de5d72bb96b5ad2e Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 8 Nov 2023 08:52:44 +0100 Subject: [PATCH 11/39] More resilient to errors. --- example/flask_rp/application.py | 2 +- src/idpyoidc/client/client_auth.py | 2 ++ src/idpyoidc/client/oauth2/utils.py | 8 ++++++-- src/idpyoidc/server/endpoint.py | 7 +++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/example/flask_rp/application.py b/example/flask_rp/application.py index cf58d426..cac411f1 100644 --- a/example/flask_rp/application.py +++ b/example/flask_rp/application.py @@ -16,7 +16,7 @@ def init_oidc_rp_handler(app): if _rp_conf.key_conf: _kj = init_key_jar(**_rp_conf.key_conf) _path = _rp_conf.key_conf['public_path'] - # removes ./ and / from the begin of the string + # removes ./ and / from the begining of the string _path = re.sub('^(.)/', '', _path) else: _kj = KeyJar() diff --git a/src/idpyoidc/client/client_auth.py b/src/idpyoidc/client/client_auth.py index 49ddc3b3..dd619c7f 100755 --- a/src/idpyoidc/client/client_auth.py +++ b/src/idpyoidc/client/client_auth.py @@ -675,6 +675,8 @@ def single_authn_setup(name, spec): else: if spec is None: cls = get_client_authn_class(name) + if cls is None: + cls = importer(name) elif isinstance(spec, str): cls = importer(spec) else: diff --git a/src/idpyoidc/client/oauth2/utils.py b/src/idpyoidc/client/oauth2/utils.py index 1933a2d0..4c277f38 100644 --- a/src/idpyoidc/client/oauth2/utils.py +++ b/src/idpyoidc/client/oauth2/utils.py @@ -76,8 +76,12 @@ def pick_redirect_uri( if redirect_uris: redirect_uri = redirect_uris[0] else: - logger.error("No redirect_uri") - raise MissingRequiredAttribute("redirect_uri") + redirect_uris = context.get_preference("redirect_uris", []) + if redirect_uris: + redirect_uri = redirect_uris[0] + else: + logger.error("No redirect_uri") + raise MissingRequiredAttribute("redirect_uri") return redirect_uri diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index 1c3d7119..9d624287 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -229,14 +229,13 @@ def parse_request( # Verify that the client is allowed to do this auth_info = self.client_authentication(req, http_info, endpoint=self, **kwargs) - if "client_id" in auth_info: - req["client_id"] = auth_info["client_id"] + _client_id = auth_info.get("client_id", "") + if _client_id: + req["client_id"] = _client_id _auth_method = auth_info.get("method") if _auth_method and _auth_method not in ["public", "none"]: req["authenticated"] = True - - _client_id = auth_info["client_id"] else: _client_id = req.get("client_id") From bff6b2370ef9eb4cb35b0839a903592bd1bf5102 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 9 Nov 2023 11:33:10 +0100 Subject: [PATCH 12/39] Is extremely confusing when an exception is rather the rule rather than the exception. --- src/idpyoidc/client/oauth2/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/idpyoidc/client/oauth2/__init__.py b/src/idpyoidc/client/oauth2/__init__.py index d28423ef..56db9614 100755 --- a/src/idpyoidc/client/oauth2/__init__.py +++ b/src/idpyoidc/client/oauth2/__init__.py @@ -160,6 +160,10 @@ def get_response( :param kwargs: :return: """ + _data = kwargs.get("data") + if _data and not body: + body = _data + try: resp = self.httpc(method, url, data=body, headers=headers, **self.httpc_params) except Exception as err: @@ -215,14 +219,10 @@ def service_request( logger.debug(REQUEST_INFO.format(url, method, body, headers)) - try: - response = service.get_response_ext( - url, method, body, response_body_type, headers, **kwargs - ) - except AttributeError: - response = self.get_response( - service, url, method, body, response_body_type, headers, **kwargs - ) + _get_response_func = getattr(self, "get_response_ext", getattr(self, "get_response")) + response = _get_response_func( + service, url, method, body, response_body_type, headers, **kwargs + ) if "error" in response: pass From d7527970358234943f3693995e19a9056e2d7a85 Mon Sep 17 00:00:00 2001 From: roland Date: Sat, 11 Nov 2023 14:01:45 +0100 Subject: [PATCH 13/39] Accept a list as a response --- src/idpyoidc/client/service.py | 18 +++++++++++++----- tests/test_server_20d_client_authn.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/idpyoidc/client/service.py b/src/idpyoidc/client/service.py index b7a310c5..781e1da8 100644 --- a/src/idpyoidc/client/service.py +++ b/src/idpyoidc/client/service.py @@ -495,7 +495,7 @@ def get_urlinfo(info): def post_parse_response(self, response, **kwargs): """ - This method does post processing of the service response. + This method does post-processing of the service response. Each service have their own version of this method. :param response: The service response @@ -541,6 +541,9 @@ def _do_jwt(self, info): def _do_response(self, info, sformat, **kwargs): _context = self.upstream_get("context") + if isinstance(info, list): # Don't have support for sformat=list + return info + try: resp = self.response_cls().deserialize(info, sformat, iss=_context.issuer, **kwargs) except Exception as err: @@ -568,7 +571,7 @@ def parse_response( state: Optional[str] = "", behaviour_args: Optional[dict] = None, **kwargs, - ): + ) : """ This the start of a pipeline that will: @@ -630,7 +633,9 @@ def parse_response( LOGGER.debug("response_cls: %s", self.response_cls.__name__) if resp is None: - if not info: + if self.response_cls == list and info == []: + return info + elif not info: LOGGER.error("Missing or faulty response") raise ResponseError("Missing or faulty response") @@ -638,14 +643,17 @@ def parse_response( resp = info else: resp = self._do_response(info, sformat, **kwargs) - LOGGER.debug('Initial response parsing => "%s"', resp.to_dict()) + if isinstance(resp, Message): + LOGGER.debug(f'Initial response parsing => "{resp.to_dict()}"') + else: + LOGGER.debug(f'Initial response parsing => "{resp}"') # is this an error message if sformat == "text": pass elif is_error_message(resp): LOGGER.debug("Error response: %s", resp) - else: + elif isinstance(resp, Message): vargs = self.gather_verify_arguments(response=resp, behaviour_args=behaviour_args) LOGGER.debug("Verify response with %s", vargs) try: diff --git a/tests/test_server_20d_client_authn.py b/tests/test_server_20d_client_authn.py index 21beb359..e6a8f792 100755 --- a/tests/test_server_20d_client_authn.py +++ b/tests/test_server_20d_client_authn.py @@ -358,7 +358,7 @@ def test_jws_authn_method_aud_token_endpoint(self): def test_jws_authn_method_aud_not_me(self): client_keyjar = KeyJar() client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point + # The only own key the client has at this point client_keyjar.add_symmetric("", client_secret, ["sig"]) _jwt = JWT(client_keyjar, iss=client_id, sign_alg="HS256") From 578e9f47e6ff52e41ef1059e39a35927a40d53bd Mon Sep 17 00:00:00 2001 From: roland Date: Sun, 19 Nov 2023 08:01:09 +0100 Subject: [PATCH 14/39] More debug messages --- example/flask_op/private/cookie_jwks.json | 2 +- example/flask_rp/templates/opbyuid.html | 2 +- src/idpyoidc/client/client_auth.py | 13 ++++----- src/idpyoidc/client/current.py | 3 +++ src/idpyoidc/client/oauth2/add_on/par.py | 27 ++++++++++++++++--- .../client/oauth2/stand_alone_client.py | 1 + src/idpyoidc/server/client_authn.py | 10 ++++++- src/idpyoidc/server/endpoint.py | 3 +++ 8 files changed, 48 insertions(+), 13 deletions(-) diff --git a/example/flask_op/private/cookie_jwks.json b/example/flask_op/private/cookie_jwks.json index 1526761c..528741ff 100644 --- a/example/flask_op/private/cookie_jwks.json +++ b/example/flask_op/private/cookie_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "LrU7gu0Jcj_3XJ0cPeUuxA0-jq5H792-"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "FQguegtRW6c0fXxDhke8dIg9QDddiAYX"}]} \ No newline at end of file +{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "GCizp3ewVRV0VZEef3VQwFve7n2QwAFI"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "QC2JxpVJXPDMpYv_h76jIrt_lA1P4KSu"}]} \ No newline at end of file diff --git a/example/flask_rp/templates/opbyuid.html b/example/flask_rp/templates/opbyuid.html index a91b6b98..5e19c7b1 100644 --- a/example/flask_rp/templates/opbyuid.html +++ b/example/flask_rp/templates/opbyuid.html @@ -19,7 +19,7 @@

By entering your unique identifier:

an issuer ID

-

Or you can chose one of the preconfigured OpenID Connect Providers

+

Or you can choose one of the preconfigured OpenID Connect Providers

an issuer ID

-

Or you can chose one of the preconfigured OpenID Connect Providers

+

Or you can choose one of the preconfigured OpenID Connect Providers