From 492f51404843bc7c2c523bd4dcf218adfdf9d6a5 Mon Sep 17 00:00:00 2001 From: peterbolha Date: Wed, 11 Oct 2023 12:06:01 +0200 Subject: [PATCH 01/17] Method for optional enforcement of resource attribute --- src/idpyoidc/server/oauth2/authorization.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index e2cd4fa7..8421bdda 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -377,6 +377,11 @@ def validate_resource_indicators_policy(request, context, **kwargs): request["scope"] = scopes return request +def optional_validate_resource_indicators_policy(request, context, **kwargs): + if "resource" not in request: + return request + + return validate_resource_indicators_policy(request, context, **kwargs) class Authorization(Endpoint): request_cls = oauth2.AuthorizationRequest From 2992c12ed423636ba05803c3d90dd554305d07b8 Mon Sep 17 00:00:00 2001 From: peterbolha Date: Tue, 24 Oct 2023 11:09:55 +0200 Subject: [PATCH 02/17] Replace wrapper with direct check in validator --- src/idpyoidc/server/oauth2/authorization.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index 8421bdda..e2cd4fa7 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -377,11 +377,6 @@ def validate_resource_indicators_policy(request, context, **kwargs): request["scope"] = scopes return request -def optional_validate_resource_indicators_policy(request, context, **kwargs): - if "resource" not in request: - return request - - return validate_resource_indicators_policy(request, context, **kwargs) class Authorization(Endpoint): request_cls = oauth2.AuthorizationRequest From 4abee99b4a55a74be8b589ba5f70c27d67285e4b Mon Sep 17 00:00:00 2001 From: Nick Mastoris Date: Mon, 12 Aug 2024 11:15:26 +0000 Subject: [PATCH 03/17] Add policy hook for client credentials grant --- src/idpyoidc/server/oauth2/introspection.py | 8 +++++- .../oauth2/token_helper/client_credentials.py | 27 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/idpyoidc/server/oauth2/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index 1a7f19c9..f95d91ae 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -74,6 +74,11 @@ def _introspect(self, token, client_id, grant): if _token_type: ret["token_type"] = _token_type + _custom_attributes = grant.claims.get("custom_attributes", None) + + if _custom_attributes: + for key, value in _custom_attributes.items(): + ret[key] = value if aud: ret["aud"] = aud @@ -140,7 +145,8 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs pass _resp.update(_info) - _resp.weed() + # Have to comment this as it deletes non-standard fields + #_resp.weed() _claims_restriction = _context.claims_interface.get_claims( _session_info["branch_id"], scopes=_token.scope, claims_release_point="introspection" diff --git a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py index fa3db7cd..1f54e9f8 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -2,9 +2,12 @@ from typing import Optional from typing import Union +from idpyoidc.exception import ImproperlyConfigured from idpyoidc.message import Message +from idpyoidc.message.oauth2 import TokenErrorResponse, AuthorizationErrorResponse from idpyoidc.message.oauth2 import CCAccessTokenRequest from idpyoidc.time_util import utc_time_sans_frac +from idpyoidc.util import importer from idpyoidc.util import sanitize from . import TokenEndpointHelper @@ -50,6 +53,9 @@ def process_request(self, req: Union[Message, dict], **kwargs): token_type = "Bearer" _allowed = _context.cdb[client_id].get("allowed_scopes", []) + + self._apply_client_credentials_filter_policy(req, _grant) + access_token = self._mint_token( token_class="access_token", grant=_grant, @@ -57,7 +63,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): client_id=_session_info["client_id"], based_on=None, scope=_allowed, - token_type=token_type, + token_type=token_type ) _resp = { @@ -77,3 +83,22 @@ def post_parse_request( request = CCAccessTokenRequest(**request.to_dict()) logger.debug("%s: %s" % (request.__class__.__name__, sanitize(request))) return request + + def _apply_client_credentials_filter_policy(self, request, grant): + _context = self.endpoint.upstream_get("context") + + policy = self.config["policy"] + function = policy[""]["function"] + kwargs = policy.get("kwargs", {}) + if isinstance(function, str): + try: + fn = importer(function) + except Exception: + raise ImproperlyConfigured(f"Error importing {function} policy function") + else: + fn = function + try: + return fn(request, context=_context, grant= grant, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} policy function: {e}") + return self.error_cls(error="server_error", error_description="Internal server error") From 2d200dea403d72f2df33b0a597dcdeeedb000a19 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Sat, 28 Sep 2024 00:24:35 +0300 Subject: [PATCH 04/17] Restructure and cleanup Signed-off-by: Ivan Kanakarakis --- src/idpyoidc/server/oauth2/introspection.py | 14 ++++++-------- .../server/oauth2/token_helper/__init__.py | 2 +- .../oauth2/token_helper/client_credentials.py | 5 ++++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/idpyoidc/server/oauth2/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index f95d91ae..0cd5747e 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -74,11 +74,6 @@ def _introspect(self, token, client_id, grant): if _token_type: ret["token_type"] = _token_type - _custom_attributes = grant.claims.get("custom_attributes", None) - - if _custom_attributes: - for key, value in _custom_attributes.items(): - ret[key] = value if aud: ret["aud"] = aud @@ -133,7 +128,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs if request["client_id"] not in aud: return {"response_args": _resp} - _info = self._introspect(_token, _session_info["client_id"], _session_info["grant"]) + _info = self._introspect(_token, _session_info["client_id"], grant) if _info is None: return {"response_args": _resp} @@ -145,8 +140,11 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs pass _resp.update(_info) - # Have to comment this as it deletes non-standard fields - #_resp.weed() + _resp.weed() + + _custom_attributes = grant.claims.get("custom_attributes") + if _custom_attributes: + _resp.update(_custom_attributes) _claims_restriction = _context.claims_interface.get_claims( _session_info["branch_id"], scopes=_token.scope, claims_release_point="introspection" diff --git a/src/idpyoidc/server/oauth2/token_helper/__init__.py b/src/idpyoidc/server/oauth2/token_helper/__init__.py index 43c2a6ca..1821b44d 100644 --- a/src/idpyoidc/server/oauth2/token_helper/__init__.py +++ b/src/idpyoidc/server/oauth2/token_helper/__init__.py @@ -15,7 +15,7 @@ class TokenEndpointHelper(object): def __init__(self, endpoint, config=None): self.endpoint = endpoint - self.config = config + self.config = config or {} self.error_cls = self.endpoint.error_cls def post_parse_request( diff --git a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py index 1f54e9f8..273ef3eb 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -87,7 +87,10 @@ def post_parse_request( def _apply_client_credentials_filter_policy(self, request, grant): _context = self.endpoint.upstream_get("context") - policy = self.config["policy"] + policy = self.config.get("policy") + if not policy: + return + function = policy[""]["function"] kwargs = policy.get("kwargs", {}) if isinstance(function, str): From c914c1ec7a4079083f5d04dec66ee4254c0ba539 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Sat, 28 Sep 2024 00:24:50 +0300 Subject: [PATCH 05/17] Filter scopes for client credentials grant Signed-off-by: Ivan Kanakarakis --- .../oauth2/token_helper/client_credentials.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py index 273ef3eb..074b4050 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -52,8 +52,14 @@ def process_request(self, req: Union[Message, dict], **kwargs): token_type = "Bearer" - _allowed = _context.cdb[client_id].get("allowed_scopes", []) - + scopes_allowed_cfg = _context.cdb[client_id].get("allowed_scopes", []) + scopes_req = req.get("scope") or [] + scopes = [ + scope + for scope in scopes_req + if scope in scopes_allowed_cfg + ] + self._apply_client_credentials_filter_policy(req, _grant) access_token = self._mint_token( @@ -62,14 +68,14 @@ def process_request(self, req: Union[Message, dict], **kwargs): session_id=_session_info["branch_id"], client_id=_session_info["client_id"], based_on=None, - scope=_allowed, + scope=scopes, token_type=token_type ) _resp = { "access_token": access_token.value, "token_type": access_token.token_class, - "scope": _allowed, + "scope": scopes, } if access_token.expires_at: From d9fc424d0bca25c81c0a5e71342b6ec0d1741391 Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Thu, 17 Oct 2024 14:11:49 +0000 Subject: [PATCH 06/17] Add denylist for redirect URI schemes --- src/idpyoidc/server/oauth2/authorization.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index e2cd4fa7..4a5904ee 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -103,7 +103,8 @@ def verify_uri( request: Union[dict, Message], uri_type: str, client_id: Optional[str] = None, - endpoint_type: Optional[str] = 'oidc' + endpoint_type: Optional[str] = 'oidc', + schemes_denylist: Optional[list] = None ): """ A redirect URI @@ -175,6 +176,8 @@ def verify_uri( # the port should not be taken into account when matching redirect URIs. client_type = client_info.get("application_type") or APPLICATION_TYPE_WEB if client_type == APPLICATION_TYPE_NATIVE: + if not has_uri_allowed_scheme(req_redirect_uri_obj, schemes_denylist or []): + raise URIError("Invalid schema in redirect URI") if is_http_uri(req_redirect_uri_obj) and is_localhost_uri(req_redirect_uri_obj): req_redirect_uri_obj = remove_port_from_uri(req_redirect_uri_obj) @@ -205,6 +208,9 @@ def is_http_uri(uri_obj: Union[ParseResult, SplitResult]) -> bool: value = uri_obj.scheme == "http" return value +def has_uri_allowed_scheme(uri_obj: Union[ParseResult, SplitResult], schemes_denylist) -> bool: + value = uri_obj.scheme not in schemes_denylist + return value def is_localhost_uri(uri_obj: Union[ParseResult, SplitResult]) -> bool: value = uri_obj.hostname in [ @@ -240,7 +246,8 @@ def join_query(base, query): def get_uri(context, request: Union[Message, dict], uri_type: str, - endpoint_type: Optional[str] = "oidc"): + endpoint_type: Optional[str] = "oidc", + schemes_denylist: Optional[list] = None): """verify that the redirect URI is reasonable. :param context: An EndpointContext instance @@ -251,7 +258,7 @@ def get_uri(context, uri = "" if uri_type in request: - verify_uri(context, request, uri_type, endpoint_type=endpoint_type) + verify_uri(context, request, uri_type, endpoint_type=endpoint_type, schemes_denylist=schemes_denylist) uri = request[uri_type] else: uris = f"{uri_type}s" @@ -412,6 +419,7 @@ def __init__(self, upstream_get, **kwargs): self.post_parse_request.append(self._post_parse_request) self.allowed_request_algorithms = AllowedAlgorithms(ALG_PARAMS) self.resource_indicators_config = kwargs.get("resource_indicators", None) + self.schemes_denylist = kwargs.get("schemes_denylist", None) def filter_request(self, context, req): return req @@ -556,7 +564,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", self.endpoint_type) + redirect_uri = get_uri(context, request, "redirect_uri", self.endpoint_type, self.schemes_denylist) except (RedirectURIError, ParameterError, URIError, UnknownClient) as err: return self.authentication_error_response( request, From e9a00d4f3dd1e58edec88086f94dae4fbcc31294 Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Fri, 18 Oct 2024 09:55:28 +0000 Subject: [PATCH 07/17] Fix typo and fix some tests regarding error handling --- src/idpyoidc/server/oauth2/authorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index 4a5904ee..62f40ca3 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -177,7 +177,7 @@ def verify_uri( client_type = client_info.get("application_type") or APPLICATION_TYPE_WEB if client_type == APPLICATION_TYPE_NATIVE: if not has_uri_allowed_scheme(req_redirect_uri_obj, schemes_denylist or []): - raise URIError("Invalid schema in redirect URI") + raise URIError("Invalid scheme in redirect URI") if is_http_uri(req_redirect_uri_obj) and is_localhost_uri(req_redirect_uri_obj): req_redirect_uri_obj = remove_port_from_uri(req_redirect_uri_obj) From dd1f1dd482d41e876e03ff49e07ebbd6eae94ff9 Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Sun, 16 Feb 2025 12:32:35 +0000 Subject: [PATCH 08/17] Support resource indicators --- src/idpyoidc/message/oauth2/__init__.py | 1 + src/idpyoidc/server/authz/__init__.py | 5 +- src/idpyoidc/server/oauth2/authorization.py | 100 ++-- src/idpyoidc/server/oauth2/token.py | 1 + .../server/oauth2/token_helper/__init__.py | 61 +- .../oauth2/token_helper/access_token.py | 47 +- .../oauth2/token_helper/client_credentials.py | 91 ++- .../oauth2/token_helper/token_exchange.py | 82 ++- .../server/oidc/token_helper/access_token.py | 73 +++ ...st_server_24_oauth2_resource_indicators.py | 549 ++++++++++++++++-- tests/test_server_24_oauth2_token_endpoint.py | 154 +++++ tests/test_server_36_oauth2_token_exchange.py | 87 ++- tests/test_tandem_oauth2_token_exchange.py | 13 +- 13 files changed, 1080 insertions(+), 184 deletions(-) diff --git a/src/idpyoidc/message/oauth2/__init__.py b/src/idpyoidc/message/oauth2/__init__.py index 788fe8c5..6514809b 100644 --- a/src/idpyoidc/message/oauth2/__init__.py +++ b/src/idpyoidc/message/oauth2/__init__.py @@ -108,6 +108,7 @@ class AccessTokenRequest(Message): "client_id": SINGLE_OPTIONAL_STRING, "client_secret": SINGLE_OPTIONAL_STRING, "state": SINGLE_OPTIONAL_STRING, + "resource": OPTIONAL_LIST_OF_STRINGS, } c_default = {"grant_type": "authorization_code"} diff --git a/src/idpyoidc/server/authz/__init__.py b/src/idpyoidc/server/authz/__init__.py index 7752e531..e9427a48 100755 --- a/src/idpyoidc/server/authz/__init__.py +++ b/src/idpyoidc/server/authz/__init__.py @@ -76,9 +76,10 @@ def __call__( else: setattr(grant, key, val) - if resources is None: + # Grant.resources may already has value + if grant.resources is None and resources is None: grant.resources = [_client_id] - else: + elif resources is not None: grant.resources = resources # Scope handling. If allowed scopes are defined for the client filter using that diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index 62f40ca3..664e68c5 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -27,7 +27,7 @@ from idpyoidc.message.oauth2 import AuthorizationRequest from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE from idpyoidc.message.oidc import APPLICATION_TYPE_WEB -from idpyoidc.message.oidc import AuthorizationResponse +from idpyoidc.message.oidc import AuthorizationResponse, TokenErrorResponse from idpyoidc.message.oidc import verified_claim_name from idpyoidc.server.authn_event import create_authn_event from idpyoidc.server.cookie_handler import compute_session_state @@ -41,6 +41,7 @@ from idpyoidc.server.exception import ToOld from idpyoidc.server.exception import UnAuthorizedClientScope from idpyoidc.server.exception import UnknownClient +from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.session import Revoked from idpyoidc.server.token.exception import UnknownToken from idpyoidc.server.user_authn.authn_context import pick_auth @@ -337,54 +338,6 @@ def check_unknown_scopes_policy(request_info, client_id, context): logger.warning(f"{client_id} requested unauthorized scopes: {diff}") raise UnAuthorizedClientScope() - -def validate_resource_indicators_policy(request, context, **kwargs): - if "resource" not in request: - return request - - resource_servers_per_client = kwargs["resource_servers_per_client"] - client_id = request["client_id"] - - if ( - isinstance(resource_servers_per_client, dict) - and client_id not in resource_servers_per_client - ): - return oauth2.AuthorizationErrorResponse( - error="invalid_target", - error_description=f"Resources for client {client_id} not found", - ) - - if isinstance(resource_servers_per_client, dict): - permitted_resources = [res for res in resource_servers_per_client[client_id]] - else: - permitted_resources = [res for res in resource_servers_per_client] - - common_resources = list(set(request["resource"]).intersection(set(permitted_resources))) - if not common_resources: - return oauth2.AuthorizationErrorResponse( - error="invalid_target", - error_description=f"Invalid resource requested by client {client_id}", - ) - - common_resources = [r for r in common_resources if r in context.cdb.keys()] - if not common_resources: - return oauth2.AuthorizationErrorResponse( - error="invalid_target", - error_description=f"Invalid resource requested by client {client_id}", - ) - - if client_id not in common_resources: - common_resources.append(client_id) - - request["resource"] = common_resources - - permitted_scopes = [context.cdb[r]["allowed_scopes"] for r in common_resources] - permitted_scopes = [r for res in permitted_scopes for r in res] - scopes = list(set(request.get("scope", [])).intersection(set(permitted_scopes))) - request["scope"] = scopes - return request - - class Authorization(Endpoint): request_cls = oauth2.AuthorizationRequest response_cls = oauth2.AuthorizationResponse @@ -574,19 +527,51 @@ def _post_parse_request(self, request, client_id, context, **kwargs): else: request["redirect_uri"] = redirect_uri - if ( - "resource_indicators" in _cinfo - and "authorization_code" in _cinfo["resource_indicators"] - ): - resource_indicators_config = _cinfo["resource_indicators"]["authorization_code"] - else: - resource_indicators_config = self.resource_indicators_config + resource_indicators_config = None + # check if enable_resource_indicators is enabled and resource parameter exists + if request.get("resource") is not None and context.conf.endpoint.get("authorization").get("kwargs").get("enable_resource_indicators"): + if "resource_indicators" in _cinfo: + resource_indicators_config = _cinfo["resource_indicators"] + if client_id in request.get("resource"): + if resource_indicators_config == None: + resource_indicators_config = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [ + client_id + ] + } + } + } + else: + # Ensure the structure exists + if "policy" in resource_indicators_config and "kwargs" in resource_indicators_config["policy"]: + resource_indicators_config["policy"]["kwargs"].setdefault("resource_servers_per_client", []).append(client_id) + else: + # If the structure is somehow not complete, initialize it + resource_indicators_config["policy"] = { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [ + client_id + ] + } + } if resource_indicators_config is not None: if "policy" not in resource_indicators_config: policy = {"policy": {"function": validate_resource_indicators_policy}} resource_indicators_config.update(policy) + request = self._enforce_resource_indicators_policy(request, resource_indicators_config) + + if "error" in request: + return self.authentication_error_response( + request, + error=request["error"], + error_description=request["error_description"], + ) return request @@ -597,9 +582,6 @@ def _enforce_resource_indicators_policy(self, request, config): function = policy["function"] kwargs = policy.get("kwargs", {}) - if kwargs.get("resource_servers_per_client", None) is None: - kwargs["resource_servers_per_client"] = {request["client_id"]: request["client_id"]} - if isinstance(function, str): try: fn = importer(function) diff --git a/src/idpyoidc/server/oauth2/token.py b/src/idpyoidc/server/oauth2/token.py index 30d34fb5..19091842 100755 --- a/src/idpyoidc/server/oauth2/token.py +++ b/src/idpyoidc/server/oauth2/token.py @@ -59,6 +59,7 @@ def __init__(self, upstream_get, new_refresh_token=False, **kwargs): # list(self.grant_type_helper.keys())) self.revoke_refresh_on_issue = kwargs.get("revoke_refresh_on_issue", False) self.resource_indicators_config = kwargs.get("resource_indicators", None) + self.enable_resource_indicators = kwargs.get("enable_resource_indicators", False) def configure_types(self, helpers, default_helpers): if helpers is None: diff --git a/src/idpyoidc/server/oauth2/token_helper/__init__.py b/src/idpyoidc/server/oauth2/token_helper/__init__.py index 1821b44d..42978733 100644 --- a/src/idpyoidc/server/oauth2/token_helper/__init__.py +++ b/src/idpyoidc/server/oauth2/token_helper/__init__.py @@ -86,15 +86,12 @@ def _mint_token( def validate_resource_indicators_policy(request, context, **kwargs): if "resource" not in request: - return TokenErrorResponse( - error="invalid_target", - error_description="Missing resource parameter", - ) + return request client_id = request["client_id"] resource_servers_per_client = kwargs.get("resource_servers_per_client", []) - + if ( isinstance(resource_servers_per_client, dict) and client_id not in resource_servers_per_client @@ -103,44 +100,58 @@ def validate_resource_indicators_policy(request, context, **kwargs): error="invalid_target", error_description=f"Resources for client {client_id} not found", ) - - if isinstance(resource_servers_per_client, dict): - permitted_resources = [res for res in resource_servers_per_client[client_id]] - else: - permitted_resources = [res for res in resource_servers_per_client] - - common_resources = list(set(request["resource"]).intersection(set(permitted_resources))) - if not common_resources: + # Check if request["resource"] is a string + if isinstance(request["resource"], str): + # If it's a string, convert it to a list + request["resource"] = [request["resource"]] + + permitted_resources = [res for res in resource_servers_per_client] + if client_id not in permitted_resources: + permitted_resources.append(client_id) + requested_resources = set(request["resource"]) + # Check if all requested resources are in permitted resources + if not requested_resources.issubset(permitted_resources): return TokenErrorResponse( error="invalid_target", - error_description=f"Invalid resource requested by client {client_id}", + error_description=f"One or more invalid resources requested by client {client_id}", ) - common_resources = [r for r in common_resources if r in context.cdb.keys()] - if not common_resources: + # Find the common resources between the request and permitted resources + common_resources_intersect = list(requested_resources.intersection(permitted_resources)) + + # Further filter common resources based on whether they exist in the context's CDB + common_resources = [r for r in common_resources_intersect if r in context.cdb.keys()] + + if set(common_resources) != set(common_resources_intersect): return TokenErrorResponse( error="invalid_target", error_description=f"Invalid resource requested by client {client_id}", ) - if client_id not in common_resources: + if client_id not in common_resources and client_id in requested_resources: common_resources.append(client_id) request["resource"] = common_resources - - permitted_scopes = [context.cdb[r]["allowed_scopes"] for r in common_resources] - permitted_scopes = [r for res in permitted_scopes for r in res] + permitted_scopes = [] + for r in common_resources: + try: + # Only proceed if r exists in context.cdb and is a dictionary + if isinstance(context.cdb.get(r), dict): + permitted_scopes.append(context.cdb[r]["allowed_scopes"]) + except KeyError: + # Handle the case where "allowed_scopes" is missing + logger.warning(f"'allowed_scopes' missing for resource {r}") + except Exception as e: + # Handle other unexpected exceptions + logger.error(f"Unexpected error for resource {r}: {e}") + if permitted_scopes: + permitted_scopes = [r for res in permitted_scopes for r in res] scopes = list(set(request.get("scope", [])).intersection(set(permitted_scopes))) request["scope"] = scopes return request def validate_token_exchange_policy(request, context, subject_token, **kwargs): - if "resource" in request: - resource = kwargs.get("resource", []) - if not set(request["resource"]).issubset(set(resource)): - return TokenErrorResponse(error="invalid_target", error_description="Unknown resource") - if "audience" in request: if request["subject_token_type"] == "urn:ietf:params:oauth:token-type:refresh_token": return TokenErrorResponse( diff --git a/src/idpyoidc/server/oauth2/token_helper/access_token.py b/src/idpyoidc/server/oauth2/token_helper/access_token.py index a5c94c2c..2af9b44e 100755 --- a/src/idpyoidc/server/oauth2/token_helper/access_token.py +++ b/src/idpyoidc/server/oauth2/token_helper/access_token.py @@ -14,7 +14,7 @@ from ...session.token import AuthorizationCode from ...token import UnknownToken from . import TokenEndpointHelper -from . import validate_resource_indicators_policy +from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy logger = logging.getLogger(__name__) @@ -49,11 +49,36 @@ def process_request(self, req: Union[Message, dict], **kwargs): return self.error_cls(error="invalid_grant", error_description="Wrong client") _cinfo = self.endpoint.upstream_get("context").cdb.get(client_id) - - if "resource_indicators" in _cinfo and "access_token" in _cinfo["resource_indicators"]: - resource_indicators_config = _cinfo["resource_indicators"]["access_token"] - else: - resource_indicators_config = self.endpoint.kwargs.get("resource_indicators", None) + resource_indicators_config = None + + # check if enable_resource_indicators is enabled and resource parameter exists + if req.get("resource") is not None and self.endpoint.kwargs.get("enable_resource_indicators"): + if "resource_indicators" in _cinfo: + resource_indicators_config = _cinfo["resource_indicators"] + if client_id in req.get("resource"): + if resource_indicators_config == None: + resource_indicators_config = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [ + client_id + ] + } + } + } + else: + # Ensure the structure exists + if "policy" in resource_indicators_config and "kwargs" in resource_indicators_config["policy"]: + resource_indicators_config["policy"]["kwargs"].setdefault("resource_servers_per_client", []).append(client_id) + else: + # If the structure is somehow not complete, initialize it + resource_indicators_config["policy"] = { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [client_id] + } + } if resource_indicators_config is not None: if "policy" not in resource_indicators_config: @@ -110,17 +135,17 @@ def process_request(self, req: Union[Message, dict], **kwargs): "scope": scope, } - if "access_token" in _supports_minting: - + if "access_token" in _supports_minting: resources = req.get("resource", None) - if resources: + if resources and resource_indicators_config is not None: token_args = {"resources": resources} else: - token_args = {} + # have to set it to blank, otherwise resources' token will have the value coming from the request + token_args = {"resources": [""]} _aud = grant.authorization_request.get("audience") if _aud: - token_args["aud"] = _aud + token_args = {"aud": _aud} try: token = self._mint_token( diff --git a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py index 074b4050..0fb40d05 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -11,6 +11,7 @@ from idpyoidc.util import sanitize from . import TokenEndpointHelper +from . import validate_resource_indicators_policy logger = logging.getLogger(__name__) @@ -48,34 +49,75 @@ def process_request(self, req: Union[Message, dict], **kwargs): branch_id = _mngr.add_grant(["client_credentials", client_id]) _session_info = _mngr.get_session_info(branch_id) + _cinfo = _context.cdb.get(client_id) + resource_indicators_config = None + resources = None + token_args = None + + # check if enable_resource_indicators is enabled and resource parameter exists + if req.get("resource") is not None and self.endpoint.kwargs.get("enable_resource_indicators"): + if "resource_indicators" in _cinfo: + resource_indicators_config = _cinfo["resource_indicators"] + if client_id in req.get("resource"): + if resource_indicators_config == None: + resource_indicators_config = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [ + client_id + ] + } + } + } + else: + # Ensure the structure exists + if "policy" in resource_indicators_config and "kwargs" in resource_indicators_config["policy"]: + resource_indicators_config["policy"]["kwargs"].setdefault("resource_servers_per_client", []).append(client_id) + else: + # If the structure is somehow not complete, initialize it + resource_indicators_config["policy"] = { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [client_id] + } + } + + if resource_indicators_config is not None: + if "policy" not in resource_indicators_config: + policy = {"policy": {"function": validate_resource_indicators_policy}} + resource_indicators_config.update(policy) + + req = self._enforce_resource_indicators_policy(req, resource_indicators_config) + + if isinstance(req, TokenErrorResponse) or isinstance(req, AuthorizationErrorResponse): + return req + + resources = req.get("resource", None) + if resources: + token_args = {"resources": resources} + _grant = _session_info["grant"] token_type = "Bearer" - scopes_allowed_cfg = _context.cdb[client_id].get("allowed_scopes", []) - scopes_req = req.get("scope") or [] - scopes = [ - scope - for scope in scopes_req - if scope in scopes_allowed_cfg - ] - + _allowed = _context.cdb[client_id].get("allowed_scopes", []) self._apply_client_credentials_filter_policy(req, _grant) - access_token = self._mint_token( token_class="access_token", grant=_grant, session_id=_session_info["branch_id"], client_id=_session_info["client_id"], based_on=None, - scope=scopes, - token_type=token_type + scope=_allowed, + token_type=token_type, + token_args=token_args, ) _resp = { "access_token": access_token.value, "token_type": access_token.token_class, - "scope": scopes, + "scope": _allowed, } if access_token.expires_at: @@ -89,14 +131,31 @@ def post_parse_request( request = CCAccessTokenRequest(**request.to_dict()) logger.debug("%s: %s" % (request.__class__.__name__, sanitize(request))) return request + + def _enforce_resource_indicators_policy(self, request, config): + _context = self.endpoint.upstream_get("context") + + policy = config["policy"] + function = policy["function"] + kwargs = policy.get("kwargs", {}) + + if isinstance(function, str): + try: + fn = importer(function) + except Exception: + raise ImproperlyConfigured(f"Error importing {function} policy function") + else: + fn = function + try: + return fn(request, context=_context, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} policy function: {e}") + return self.error_cls(error="server_error", error_description="Internal server error") def _apply_client_credentials_filter_policy(self, request, grant): _context = self.endpoint.upstream_get("context") - policy = self.config.get("policy") - if not policy: - return - + policy = self.config["policy"] function = policy[""]["function"] kwargs = policy.get("kwargs", {}) if isinstance(function, str): diff --git a/src/idpyoidc/server/oauth2/token_helper/token_exchange.py b/src/idpyoidc/server/oauth2/token_helper/token_exchange.py index cc81fb0d..bb9a9a55 100755 --- a/src/idpyoidc/server/oauth2/token_helper/token_exchange.py +++ b/src/idpyoidc/server/oauth2/token_helper/token_exchange.py @@ -20,6 +20,7 @@ from idpyoidc.util import importer from . import TokenEndpointHelper +from . import validate_resource_indicators_policy from . import validate_token_exchange_policy logger = logging.getLogger(__name__) @@ -89,11 +90,11 @@ def post_parse_request(self, request, client_id="", **kwargs): return self.error_cls( error="invalid_request", error_description="Subject token inactive" ) - + resp = self._enforce_policy(request, token, config) if isinstance(resp, TokenErrorResponse): return resp - + scopes = resp.get("scope", []) scopes = _context.scopes_handler.filter_scopes(scopes, client_id=resp["client_id"]) @@ -113,6 +114,7 @@ def post_parse_request(self, request, client_id="", **kwargs): error_description="Exchanging this subject token to refresh token forbidden", ) + return resp def _enforce_policy(self, request, token, config): @@ -171,6 +173,26 @@ def _enforce_policy(self, request, token, config): logger.error(f"Error while executing the {fn} policy function: {e}") return self.error_cls(error="server_error", error_description="Internal server error") + def _enforce_resource_indicators_policy(self, request, config): + _context = self.endpoint.upstream_get("context") + + policy = config["policy"] + function = policy["function"] + kwargs = policy.get("kwargs", {}) + + if isinstance(function, str): + try: + fn = importer(function) + except Exception: + raise ImproperlyConfigured(f"Error importing {function} policy function") + else: + fn = function + try: + return fn(request, context=_context, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} policy function: {e}") + return self.error_cls(error="server_error", error_description="Internal server error") + def token_exchange_response(self, token, issued_token_type): response_args = {} response_args["access_token"] = token.value @@ -243,15 +265,55 @@ def process_request(self, request, **kwargs): error="server_error", error_description="Internal server error" ) - resources = request.get("resource") - if resources and request.get("audience"): - resources = list(set(resources + request.get("audience"))) - else: - resources = request.get("audience") - + client_id = request["client_id"] + _cinfo = _context.cdb.get(client_id) + resource_indicators_config = None _token_args = None - if resources: - _token_args = {"resources": resources} + # check if resource parameter exists + if request.get("resource") is not None: + if "resource_indicators" in _cinfo: + resource_indicators_config = _cinfo["resource_indicators"] + if client_id in request.get("resource"): + if resource_indicators_config == None: + resource_indicators_config = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [ + client_id + ] + } + } + } + else: + # Ensure the structure exists + if "policy" in resource_indicators_config and "kwargs" in resource_indicators_config["policy"]: + resource_indicators_config["policy"]["kwargs"].setdefault("resource_servers_per_client", []).append(client_id) + else: + # If the structure is somehow not complete, initialize it + resource_indicators_config["policy"] = { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [client_id] + } + } + + + if resource_indicators_config is not None: + if "policy" not in resource_indicators_config: + policy = {"policy": {"function": validate_resource_indicators_policy}} + resource_indicators_config.update(policy) + request = self._enforce_resource_indicators_policy(request, resource_indicators_config) + if isinstance(request, TokenErrorResponse): + return request + + resources = request.get("resource", None) + if resources: + _token_args = {"resources": resources} + + requested_resources = request.get("resource") or [] + requested_aud = request.get("audience") or [] + resources = list(set(requested_resources + requested_aud)) try: new_token = self._mint_token( diff --git a/src/idpyoidc/server/oidc/token_helper/access_token.py b/src/idpyoidc/server/oidc/token_helper/access_token.py index 2594748e..6f2668d9 100755 --- a/src/idpyoidc/server/oidc/token_helper/access_token.py +++ b/src/idpyoidc/server/oidc/token_helper/access_token.py @@ -5,14 +5,20 @@ from cryptojwt.jwe.exception import JWEException from cryptojwt.jws.exception import NoSuitableSigningKeys from cryptojwt.jwt import utc_time_sans_frac +from cryptojwt.utils import importer +from idpyoidc.exception import ImproperlyConfigured from idpyoidc.message import Message +from idpyoidc.message.oauth2 import TokenErrorResponse from idpyoidc.server.oauth2.token_helper import TokenEndpointHelper +from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.session.token import AuthorizationCode from idpyoidc.server.session.token import MintingNotAllowed from idpyoidc.server.token.exception import UnknownToken from idpyoidc.util import sanitize + + logger = logging.getLogger(__name__) @@ -104,6 +110,52 @@ def process_request(self, req: Union[Message, dict], **kwargs): } if "access_token" in _supports_minting: + # check if enable_resource_indicators is enabled and resource parameter exists + _cinfo = self.endpoint.upstream_get("context").cdb.get(client_id) + resource_indicators_config = None + if req.get("resource") is not None and self.endpoint.kwargs.get("enable_resource_indicators"): + if "resource_indicators" in _cinfo: + resource_indicators_config = _cinfo["resource_indicators"] + if client_id in req.get("resource"): + if resource_indicators_config == None: + resource_indicators_config = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [ + client_id + ] + } + } + } + else: + # Ensure the structure exists + if "policy" in resource_indicators_config and "kwargs" in resource_indicators_config["policy"]: + resource_indicators_config["policy"]["kwargs"].setdefault("resource_servers_per_client", []).append(client_id) + else: + # If the structure is somehow not complete, initialize it + resource_indicators_config["policy"] = { + "function": validate_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": [client_id] + } + } + + if resource_indicators_config is not None: + if "policy" not in resource_indicators_config: + policy = {"policy": {"function": validate_resource_indicators_policy}} + resource_indicators_config.update(policy) + + req = self._enforce_resource_indicators_policy(req, resource_indicators_config) + + if isinstance(req, TokenErrorResponse): + return req + + # Maybe there is a different resource at the request from auth code. + # We must take it into account + token_args = {} + if(req.get("resource") is not None and grant.resources != req.get("resource")): + token_args["resources"] = req["resource"] try: token = self._mint_token( token_class="access_token", @@ -112,6 +164,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): client_id=_session_info["client_id"], based_on=_based_on, token_type=token_type, + token_args=token_args, ) except MintingNotAllowed as err: logger.warning(err) @@ -206,3 +259,23 @@ def post_parse_request( logger.debug("%s: %s" % (request.__class__.__name__, sanitize(request))) return request + + def _enforce_resource_indicators_policy(self, request, config): + _context = self.endpoint.upstream_get("context") + + policy = config["policy"] + function = policy["function"] + kwargs = policy.get("kwargs", {}) + + if isinstance(function, str): + try: + fn = importer(function) + except Exception: + raise ImproperlyConfigured(f"Error importing {function} policy function") + else: + fn = function + try: + return fn(request, context=_context, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} policy function: {e}") + return self.error_cls(error="server_error", error_description="Internal server error") diff --git a/tests/test_server_24_oauth2_resource_indicators.py b/tests/test_server_24_oauth2_resource_indicators.py index 14e6a032..14f66676 100644 --- a/tests/test_server_24_oauth2_resource_indicators.py +++ b/tests/test_server_24_oauth2_resource_indicators.py @@ -331,36 +331,20 @@ def get_cookie_value(cookie=None, name=None): "claims_parameter_supported": True, "request_parameter_supported": True, "request_uri_parameter_supported": True, - "resource_indicators": { - "policy": { - "function": validate_authorization_resource_indicators_policy, - "kwargs": { - "resource_servers_per_client": { - "client_1": ["client_1", "client_2"], - }, - }, - } - }, + "resource_indicators_supported": True, }, }, "token": { "path": "token", "class": Token, "kwargs": { + "enable_resource_indicators": True, "client_authn_method": [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", ], - "resource_indicators": { - "policy": { - "function": validate_token_resource_indicators_policy, - "kwargs": { - "resource_servers_per_client": {"client_1": ["client_2", "client_3"]}, - }, - } - }, }, }, }, @@ -553,62 +537,228 @@ def test_authorization_code_req(self, create_endpoint_ri_enabled): def test_authorization_code_req_per_client(self, create_endpoint_ri_disabled): """ - Test that appropriate error message is returned when resource indicators is enabled per client - for the authorization endpoint and requested resource is not permitted for client. + Test that no error message is returned when resource indicators is disabled + and resource exists at the request. """ endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { - "authorization_code": { - "policy": { - "function": validate_authorization_resource_indicators_policy, - "kwargs": {"resource_servers_per_client": ["client_3"]}, - }, + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_3"]}, }, } request = AUTH_REQ.copy() - client_id = request["client_id"] - msg = self.endpoint._post_parse_request(request, "client_1", endpoint_context) - assert "error" in msg - assert msg["error_description"] == f"Invalid resource requested by client {client_id}" + assert "error" not in msg - def test_authorization_code_req_no_resource_client(self, create_endpoint_ri_enabled): + def test_authorization_code_req_no_resource_clients(self, create_endpoint_ri_enabled): """ Test that appropriate error message is returned when resource indicators is enabled - for the authorization endpoint and permitted resources are not configured for client. + and permitted resources are not configured for client. """ + endpoint_context = self.endpoint.upstream_get("context") request = AUTH_REQ.copy() + request["resource"] = "client_3" client_id = request["client_id"] endpoint_context = self.endpoint.upstream_get("context") - self.endpoint.kwargs["resource_indicators"]["policy"]["kwargs"][ - "resource_servers_per_client" - ] = {"client_2": ["client_1"]} msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) - assert "error" in msg - assert msg["error"] == "invalid_target" - assert msg["error_description"] == f"Resources for client {client_id} not found" + assert "error" not in msg + + def test_authorization_code_resource_indicators_enabled_resource_exists(self, create_endpoint_ri_enabled): + """ + Test that no error message is returned when resource indicators is enabled + and requested resource is permitted for client. + """ + request = AUTH_REQ.copy() + client_id = request["client_id"] + endpoint_context = self.endpoint.upstream_get("context") + endpoint_context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + assert "error" not in msg + + def test_authorization_code_resource_indicators_enabled_resource_itself(self, create_endpoint_ri_enabled): + """ + Test that no error message is returned when resource indicators is enabled + and requested resource is the client itself. + """ + request = AUTH_REQ.copy() + request["resource"] = "client_1" + client_id = request["client_id"] + endpoint_context = self.endpoint.upstream_get("context") + endpoint_context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } - def test_authorization_code_req_invalid_resource_client(self, create_endpoint_ri_enabled): + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + assert "error" not in msg + + def test_authorization_code_resource_indicators_enabled_resource_itself_no_client_conf(self, create_endpoint_ri_enabled): + """ + Test that no error message is returned when resource indicators is enabled, + there is no client configuration for resource_indicators + and requested resource is the client itself. + """ + request = AUTH_REQ.copy() + request["resource"] = "client_1" + client_id = request["client_id"] + endpoint_context = self.endpoint.upstream_get("context") + + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + assert "error" not in msg + + def test_authorization_code_resource_indicators_enabled_resource_unknown(self, create_endpoint_ri_enabled): """ Test that appropriate error message is returned when resource indicators is enabled - for the authorization endpoint and requested resource is not permitted for client. + there is client configuration for resource_indicators + and requested resource is unknown for the client. """ request = AUTH_REQ.copy() request["resource"] = "client_3" client_id = request["client_id"] endpoint_context = self.endpoint.upstream_get("context") - + endpoint_context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + assert "error" in msg + assert msg["error_description"] == f"Invalid resource requested by client {client_id}" + + def test_authorization_code_resource_indicators_disabled_resource_multiple(self, create_endpoint_ri_disabled): + """ + Test that no error message is returned when resource indicators is disabled + there is client configuration for resource_indicators + and requested resource contains multiple clients. + """ + request = AUTH_REQ.copy() + request["resource"] = ["client2","client_3"] + client_id = request["client_id"] + endpoint_context = self.endpoint.upstream_get("context") + endpoint_context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + assert "error" not in msg + + def test_authorization_code_resource_indicators_enabled_resource_multiple_with_itself(self, create_endpoint_ri_enabled): + """ + Test that no error message is returned when resource indicators is enabled + there is no client configuration for resource_indicators + and requested resource contains the client itself, among others that are unknown. + """ + request = AUTH_REQ.copy() + request["resource"] = ["client_1", "client_3"] + client_id = request["client_id"] + endpoint_context = self.endpoint.upstream_get("context") + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + assert "error" not in msg + def test_authorization_code_resource_indicators_enabled_resource_multiple_unknown(self, create_endpoint_ri_enabled): + """ + Test that appropriate error message is returned when resource indicators is enabled + there is client configuration for resource_indicators + and requested resource contains multiple unknown clients. + """ + request = AUTH_REQ.copy() + request["resource"] = ["client_3", "client_4"] + client_id = request["client_id"] + endpoint_context = self.endpoint.upstream_get("context") + endpoint_context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) assert "error" in msg - assert msg["error"] == "invalid_target" assert msg["error_description"] == f"Invalid resource requested by client {client_id}" - def test_access_token_req(self, create_endpoint_ri_enabled): + def test_access_token_req_disabled_resource_multiple_resource_no_client_conf(self, create_endpoint_ri_enabled): + """ + Test that no error message is returned (neither "aud" claim) + when resource indicators is enabled + for the token endpoint, without client configuration for resource_indicators + and there is a requested resource. + """ + + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] + _token_request["resource"] = ["client_3"] + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert "aud" not in access_token + + def test_access_token_req_disabled_resource_multiple_resource_client_conf(self, create_endpoint_ri_disabled): + """ + Test that no error message is returned (neither "aud" claim) + when resource indicators is disabled + for the token endpoint and there is requested resource. + """ + + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] + _token_request["resource"] = ["client_3", "client_4"] + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert "aud" not in access_token + + def test_access_token_req_resource_known(self, create_endpoint_ri_enabled): """ - Test successful access_token request when resource indicators is enabled. + Test successful access_token request when resource indicators is enabled + containing also an aud claim with the appropriate client. """ self.endpoint.upstream_get("context").cdb["client_3"] = { "client_id": "client_3", @@ -616,6 +766,12 @@ def test_access_token_req(self, create_endpoint_ri_enabled): "id_token_signed_response_alg": "ES256", "allowed_scopes": ["openid"], } + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } session_id = self._create_session(AUTH_REQ) grant = self.session_manager[session_id] code = self._mint_code(grant, AUTH_REQ["client_id"]) @@ -624,6 +780,7 @@ def test_access_token_req(self, create_endpoint_ri_enabled): _token_request = TOKEN_REQ_DICT.copy() _token_request["code"] = code.value + _token_request["resource"] = ["client_2"] _req = self.token_endpoint.parse_request(_token_request) _resp = self.token_endpoint.process_request(request=_req) @@ -634,13 +791,93 @@ def test_access_token_req(self, create_endpoint_ri_enabled): sender="", ) - assert set(access_token["aud"]) == set(["client_3", "client_1"]) + assert "client_2" in access_token["aud"] + + def test_access_token_req_resource_itself(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled + requesting for resource about itself. + """ + self.endpoint.upstream_get("context").cdb["client_3"] = { + "client_id": "client_3", + "redirect_uris": [("https://rp.example.com/cb", {})], + "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid"], + } + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _token_request["resource"] = ["client_1"] + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) - def test_access_token_req_invalid_resource_client(self, create_endpoint_ri_enabled): + assert access_token["aud"] == ["client_1"] + + + def test_access_token_req_resource_itself_no_client_conf(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled + no client configuration exists for resource indicators + and the requested resource is the client itself. + """ + self.endpoint.upstream_get("context").cdb["client_3"] = { + "client_id": "client_3", + "redirect_uris": [("https://rp.example.com/cb", {})], + "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid"], + } + + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _token_request["resource"] = ["client_1"] + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert access_token["aud"] == ["client_1"] + + def test_access_token_req_invalid_multiple_resource_client_conf(self, create_endpoint_ri_enabled): """ Test that appropriate error message is returned when resource indicators is enabled for the token endpoint and requested resource is not permitted for client. """ + + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } session_id = self._create_session(AUTH_REQ) grant = self.session_manager[session_id] code = self._mint_code(grant, AUTH_REQ["client_id"]) @@ -649,7 +886,7 @@ def test_access_token_req_invalid_resource_client(self, create_endpoint_ri_enabl _token_request = TOKEN_REQ_DICT.copy() client_id = _token_request["client_id"] - _token_request["resource"] = "client_2" + _token_request["resource"] = ["client_3", "client_4"] _token_request["code"] = code.value _req = self.token_endpoint.parse_request(_token_request) @@ -658,6 +895,226 @@ def test_access_token_req_invalid_resource_client(self, create_endpoint_ri_enabl assert "error" in _resp assert _resp["error"] == "invalid_target" assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" + + def test_access_token_req_disabled_ri_multiple_resource_client_conf(self, create_endpoint_ri_disabled): + """ + Test that no error message is returned (neither an "aud" claim) + when resource indicators is disabled + for the token endpoint and requested resource contains multiple clients. + """ + + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] + _token_request["resource"] = ["client_3", "client_4"] + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + assert "aud" not in access_token + + def test_access_token_req_resource_itself_multiple_resource_one_known(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled + and the resource parameter contains at least one known resource. + """ + + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + + self.endpoint.upstream_get("context").cdb["client_3"] = { + "client_id": "client_3", + "redirect_uris": [("https://rp.example.com/cb", {})], + "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid"], + } + + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _token_request["resource"] = ["client_2", "client_3"] + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert "client_2" in access_token["aud"] + + def test_access_token_req_multiple_resource_itself(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled. + and the request contains a resource about client itself, among other unknown clients + """ + + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + + self.endpoint.upstream_get("context").cdb["client_3"] = { + "client_id": "client_3", + "redirect_uris": [("https://rp.example.com/cb", {})], + "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid"], + } + + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _token_request["resource"] = ["client_1", "client_3"] + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert "client_1" in access_token["aud"] + + def test_access_token_req_multiple_resource_itself_no_client_conf(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled, + there is no client configuration for resource indicators + and the request contains a resource about client itself, among other unknown clients + """ + + self.endpoint.upstream_get("context").cdb["client_3"] = { + "client_id": "client_3", + "redirect_uris": [("https://rp.example.com/cb", {})], + "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid"], + } + + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _token_request["resource"] = ["client_1", "client_3"] + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert "client_1" in access_token["aud"] + + def test_access_token_req_multiple_resource_known(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled + and the resource contains multiple known clients + """ + self.endpoint.upstream_get("context").cdb["client_3"] = { + "client_id": "client_3", + "redirect_uris": [("https://rp.example.com/cb", {})], + "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid"], + } + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2","client_3"]}, + }, + } + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _token_request["resource"] = ["client_2", "client_3"] + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + access_token = TokenErrorResponse().from_jwt( + _resp["response_args"]["access_token"], + self.endpoint_context.keyjar, + sender="", + ) + + assert "client_2" in access_token["aud"] + assert "client_3" in access_token["aud"] + + def test_access_token_req_invalid_multiple_resource_unknown(self, create_endpoint_ri_enabled): + """ + Test that appropriate error message is returned when resource indicators is enabled + and requested resources are not permitted for client. + """ + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_authorization_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + assert code.resources != [] + + _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] + _token_request["resource"] = ["client_3","client_4"] + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + assert "error" in _resp + assert _resp["error"] == "invalid_target" + assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" def test_create_authn_response(self, create_endpoint_ri_enabled): """ diff --git a/tests/test_server_24_oauth2_token_endpoint.py b/tests/test_server_24_oauth2_token_endpoint.py index 4d43d302..889c67e5 100644 --- a/tests/test_server_24_oauth2_token_endpoint.py +++ b/tests/test_server_24_oauth2_token_endpoint.py @@ -16,6 +16,7 @@ from idpyoidc.message.oauth2 import CCAccessTokenRequest from idpyoidc.message.oauth2 import JWTAccessToken from idpyoidc.message.oauth2 import ROPCAccessTokenRequest +from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import AccessTokenRequest from idpyoidc.message.oidc import AuthorizationRequest from idpyoidc.message.oidc import RefreshAccessTokenRequest @@ -27,6 +28,7 @@ from idpyoidc.server.configure import ASConfiguration from idpyoidc.server.exception import InvalidToken from idpyoidc.server.oauth2.authorization import Authorization +from idpyoidc.server.oauth2.authorization import validate_resource_indicators_policy from idpyoidc.server.oauth2.token import Token from idpyoidc.server.token import handler from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD @@ -129,6 +131,7 @@ def conf(): "client_secret_post", "client_secret_jwt", "private_key_jwt", + ] }, }, @@ -184,6 +187,7 @@ def create_endpoint(self, conf): "response_types": ["code", "token", "code id_token", "id_token"], "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], } + server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") self.session_manager = context.session_manager self.token_endpoint = server.get_endpoint("token") @@ -932,10 +936,21 @@ def create_endpoint(self, conf): "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], "grant_types_supported": ["client_credentials", "password"], } + context.cdb["client_2"] = { + "client_secret": "hemligt", + "redirect_uris": [("https://example.com/cb", None)], + "client_salt": "salted", + "endpoint_auth_method": "client_secret_post", + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + "grant_types_supported": ["client_credentials", "password"], + } + server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") self.session_manager = context.session_manager self.token_endpoint = server.get_endpoint("token") self.user_id = "diana" self.context = context + self.keyjar = server.keyjar def test_client_credentials(self): request = CCAccessTokenRequest( @@ -953,6 +968,145 @@ def test_client_credentials(self): "scope", "expires_in", } + + @pytest.mark.parametrize("resource", ["client2", ["client2", "client3"]]) + def test_client_credentials_resource_indicator_disabled(self, resource): + request = CCAccessTokenRequest( + client_id="client_1", + client_secret="hemligt", + grant_type="client_credentials", + scope="whatever", + resource=resource + ) + request = self.token_endpoint.parse_request(request) + response = self.token_endpoint.process_request(request) + assert set(response.keys()) == {"response_args", "cookie", "http_headers"} + assert set(response["response_args"].keys()) == { + "access_token", + "token_type", + "scope", + "expires_in", + } + + def test_client_credentials_resource_indicator_enabled(self): + self.token_endpoint.kwargs["enable_resource_indicators"] = True + request = CCAccessTokenRequest( + client_id="client_1", + client_secret="hemligt", + grant_type="client_credentials", + scope="whatever", + resource="client_2" + ) + request = self.token_endpoint.parse_request(request) + response = self.token_endpoint.process_request(request) + assert set(response.keys()) == {"response_args", "cookie", "http_headers"} + assert set(response["response_args"].keys()) == { + "access_token", + "token_type", + "scope", + "expires_in", + } + + @pytest.mark.parametrize("resource", ["client2", ["client2", "client3"],["client1", "client2"],["client2", "client4"]]) + def test_client_credentials_resource_indicator_enabled_client_conf(self, resource): + self.token_endpoint.kwargs["enable_resource_indicators"] = True + self.context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + if "client_4" in resource: + self.context.cdb["client_4"] = { + "client_secret": "hemligt", + "redirect_uris": [("https://example.com/cb", None)], + "client_salt": "salted", + "endpoint_auth_method": "client_secret_post", + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + "grant_types_supported": ["client_credentials", "password"], + } + request = CCAccessTokenRequest( + client_id="client_1", + client_secret="hemligt", + grant_type="client_credentials", + scope="whatever", + resource=["client_2"] + ) + request = self.token_endpoint.parse_request(request) + response = self.token_endpoint.process_request(request) + + assert set(response.keys()) == {"response_args", "cookie", "http_headers"} + assert set(response["response_args"].keys()) == { + "access_token", + "token_type", + "scope", + "expires_in", + } + + # Access Token + access_token = AuthorizationResponse().from_jwt( + response["response_args"]["access_token"], self.keyjar, sender="" + ) + + assert "aud" in access_token + assert "client_2" in access_token["aud"] + if "client_1" in resource: + assert "client_1" in access_token["aud"] + if "client_4" in resource: + assert "client_4" in access_token["aud"] + + @pytest.mark.parametrize("resource", ["client4", ["client3", "client4"]]) + def test_client_credentials_resource_indicator_enabled_client_conf_unknown_resource(self, resource): + self.token_endpoint.kwargs["enable_resource_indicators"] = True + self.context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + + request = CCAccessTokenRequest( + client_id="client_1", + client_secret="hemligt", + grant_type="client_credentials", + scope="whatever", + resource=resource + ) + client_id = request["client_id"] + request = self.token_endpoint.parse_request(request) + response = self.token_endpoint.process_request(request) + + assert response["error"] == "invalid_target" + assert response["error_description"] == f"Invalid resource requested by client {client_id}" + + @pytest.mark.parametrize("resource", ["client_1", ["client_1", "client_3"]]) + def test_client_credentials_resource_indicator_enabled_client_conf_itself_resource(self, resource): + self.token_endpoint.kwargs["enable_resource_indicators"] = True + self.context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + + request = CCAccessTokenRequest( + client_id="client_1", + client_secret="hemligt", + grant_type="client_credentials", + scope="whatever", + resource=resource + ) + + request = self.token_endpoint.parse_request(request) + response = self.token_endpoint.process_request(request) + # Access Token + access_token = AuthorizationResponse().from_jwt( + response["response_args"]["access_token"], self.keyjar, sender="" + ) + + assert "aud" in access_token + assert "client_1" in access_token["aud"] class TestResourceOwnerPasswordCredentialsFlow(object): diff --git a/tests/test_server_36_oauth2_token_exchange.py b/tests/test_server_36_oauth2_token_exchange.py index 5b3a5663..eef68993 100644 --- a/tests/test_server_36_oauth2_token_exchange.py +++ b/tests/test_server_36_oauth2_token_exchange.py @@ -8,6 +8,7 @@ from idpyoidc.message.oauth2 import TokenExchangeRequest from idpyoidc.message.oidc import AccessTokenRequest from idpyoidc.message.oidc import AuthorizationRequest +from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import RefreshAccessTokenRequest from idpyoidc.server import Server from idpyoidc.server.authn_event import create_authn_event @@ -15,6 +16,7 @@ from idpyoidc.server.client_authn import verify_client from idpyoidc.server.configure import ASConfiguration from idpyoidc.server.cookie_handler import CookieHandler +from idpyoidc.server.oauth2.authorization import validate_resource_indicators_policy from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD from idpyoidc.server.user_info import UserInfo from tests import CRYPT_CONFIG @@ -204,6 +206,7 @@ def create_endpoint(self): self.introspection_endpoint = server.get_endpoint("introspection") self.session_manager = self.context.session_manager self.user_id = "diana" + self.keyjar = server.keyjar def _create_session(self, auth_req, sub_type="public", sector_identifier=""): if sector_identifier: @@ -275,7 +278,7 @@ def test_token_exchange1(self, token): {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzI6aGVtbGlndA==")}}, ) _resp = self.endpoint.process_request(request=_req) - print(_resp["response_args"]) + assert set(_resp["response_args"].keys()) == { "access_token", "token_type", @@ -650,24 +653,30 @@ def test_token_exchange_fails_if_disabled(self): == "Unsupported grant_type: urn:ietf:params:oauth:grant-type:token-exchange" ) - def test_wrong_resource(self): + @pytest.mark.parametrize("resource", ["client_3", ["client_3", "client_4"]]) + def test_wrong_resource(self, resource): """ Test that requesting a token for an unknown resource fails. """ + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } conf = self.endpoint.grant_type_helper[ "urn:ietf:params:oauth:grant-type:token-exchange" ].config - conf["policy"][""]["kwargs"] = {} - conf["policy"][""]["kwargs"]["resource"] = ["https://example.com"] areq = AUTH_REQ.copy() - + areq["resource"] = resource session_id = self._create_session(areq) grant = self.context.authz(session_id, areq) code = self._mint_code(grant, areq["client_id"]) - + _token_request = TOKEN_REQ_DICT.copy() _token_request["code"] = code.value _req = self.endpoint.parse_request(_token_request) + client_id = _req["client_id"] _resp = self.endpoint.process_request(request=_req) _token_value = _resp["response_args"]["access_token"] @@ -676,17 +685,75 @@ def test_wrong_resource(self): grant_type="urn:ietf:params:oauth:grant-type:token-exchange", subject_token=_token_value, subject_token_type="urn:ietf:params:oauth:token-type:access_token", - resource=["https://unknown-resource.com/api"], + resource=resource, ) - _req = self.endpoint.parse_request( - token_exchange_req.to_urlencoded(), + token_exchange_req.to_urlencoded(doseq=True), {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, ) _resp = self.endpoint.process_request(request=_req) assert set(_resp.keys()) == {"error", "error_description"} assert _resp["error"] == "invalid_target" - assert _resp["error_description"] == "Unknown resource" + assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" + + @pytest.mark.parametrize("resource", ["client_2", ["client_2", "client_3"]]) + def test_token_exchange_req_resource(self, resource): + """ + Test that requesting a token for an known resource succeeds with the respective aud. + """ + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_2"]}, + }, + } + conf = self.endpoint.grant_type_helper[ + "urn:ietf:params:oauth:grant-type:token-exchange" + ].config + areq = AUTH_REQ.copy() + + areq["resource"] = resource + session_id = self._create_session(areq) + grant = self.context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.endpoint.parse_request(_token_request) + _resp = self.endpoint.process_request(request=_req) + + _token_value = _resp["response_args"]["access_token"] + + token_exchange_req = TokenExchangeRequest( + grant_type="urn:ietf:params:oauth:grant-type:token-exchange", + subject_token=_token_value, + subject_token_type="urn:ietf:params:oauth:token-type:access_token", + resource=resource, + ) + + _req = self.endpoint.parse_request( + token_exchange_req, + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "issued_token_type", + "scope", + } + msg = self.endpoint.do_response(request=_req, **_resp) + assert isinstance(msg, dict) + + id_token = AuthorizationResponse().from_jwt( + _resp["response_args"]["access_token"], self.keyjar, sender="" + ) + + assert "client_2" in id_token["aud"] + assert "client_3" not in id_token["aud"] + def test_refresh_token_audience(self): """ diff --git a/tests/test_tandem_oauth2_token_exchange.py b/tests/test_tandem_oauth2_token_exchange.py index 6b722d3b..1d6c0645 100644 --- a/tests/test_tandem_oauth2_token_exchange.py +++ b/tests/test_tandem_oauth2_token_exchange.py @@ -14,6 +14,7 @@ from idpyoidc.server.client_authn import verify_client from idpyoidc.server.configure import ASConfiguration from idpyoidc.server.cookie_handler import CookieHandler +from idpyoidc.server.oauth2.authorization import validate_resource_indicators_policy from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD from idpyoidc.server.user_info import UserInfo from idpyoidc.util import rndstr @@ -455,10 +456,12 @@ def test_wrong_resource(self): """ endpoint = self.server.get_endpoint("token") - conf = endpoint.grant_type_helper["urn:ietf:params:oauth:grant-type:token-exchange"].config - conf["policy"][""]["kwargs"] = {} - conf["policy"][""]["kwargs"]["resource"] = ["https://example.com"] - + self.context.cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["https://example.com"]}, + }, + } resp, _state, _scope = self.process_setup() # ****** Token Exchange Request ********** @@ -474,7 +477,7 @@ def test_wrong_resource(self): assert set(_te_resp.keys()) == {"error", "error_description"} assert _te_resp["error"] == "invalid_target" - assert _te_resp["error_description"] == "Unknown resource" + assert _te_resp["error_description"] == "Invalid resource requested by client client_1" def test_refresh_token_audience(self): """ From a99538b566b294e1421bca43cf8a13149b5e28bc Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Tue, 18 Feb 2025 08:27:15 +0000 Subject: [PATCH 09/17] add audience policy implementation --- src/idpyoidc/server/oauth2/authorization.py | 10 +++++++ src/idpyoidc/server/oauth2/introspection.py | 8 +++++ src/idpyoidc/server/oauth2/token.py | 2 ++ .../server/oauth2/token_helper/__init__.py | 30 +++++++++++++++++++ .../oauth2/token_helper/access_token.py | 8 ++++- .../oauth2/token_helper/client_credentials.py | 21 +++++++++++-- .../oauth2/token_helper/token_exchange.py | 8 +++++ .../server/oidc/token_helper/access_token.py | 7 +++++ .../server/oidc/token_helper/refresh_token.py | 9 +++++- 9 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index 664e68c5..b5a7e7fd 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -41,6 +41,7 @@ from idpyoidc.server.exception import ToOld from idpyoidc.server.exception import UnAuthorizedClientScope from idpyoidc.server.exception import UnknownClient +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.session import Revoked from idpyoidc.server.token.exception import UnknownToken @@ -921,6 +922,15 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict else: _aud_arg = {} + client_id = request["client_id"] + _cinfo = _context.cdb.get(client_id) + apply_audience_policies(request, _context, _cinfo, request.get("resource", None), grant, _context.conf.endpoint.get("authorization").get("kwargs")) + if "error" in request: + return self.authentication_error_response( + request, + error=request["error"], + error_description=request["error_description"], + ) if "code" in rtype: _code = self.mint_token( token_class="authorization_code", diff --git a/src/idpyoidc/server/oauth2/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index 0cd5747e..4a3bd600 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -5,6 +5,7 @@ from idpyoidc.message import oauth2 from idpyoidc.server.endpoint import Endpoint from idpyoidc.server.exception import ToOld +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.server.token.exception import UnknownToken from idpyoidc.server.token.exception import WrongTokenClass @@ -34,6 +35,8 @@ def __init__(self, upstream_get, **kwargs): Endpoint.__init__(self, upstream_get, **kwargs) self.offset = kwargs.get("offset", 0) self.enforce_aud_restriction = kwargs.get("enforce_audience_restriction", True) + self.audience_policies_config = kwargs.get("audience_policies", None) + self.enable_audience_policies = kwargs.get("enable_audience_policies", False) def _introspect(self, token, client_id, grant): # Make sure that the token is an access_token or a refresh_token @@ -117,6 +120,11 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs aud = grant.resources client_id = request["client_id"] + + apply_audience_policies(request, _context, _context.cdb[client_id], aud, _session_info["grant"], self.kwargs) + if "error" in request: + return {"response_args": _resp} + try: _cinfo = _context.cdb[client_id] enforce_aud_restriction = _cinfo.get( diff --git a/src/idpyoidc/server/oauth2/token.py b/src/idpyoidc/server/oauth2/token.py index 19091842..db582358 100755 --- a/src/idpyoidc/server/oauth2/token.py +++ b/src/idpyoidc/server/oauth2/token.py @@ -60,6 +60,8 @@ def __init__(self, upstream_get, new_refresh_token=False, **kwargs): self.revoke_refresh_on_issue = kwargs.get("revoke_refresh_on_issue", False) self.resource_indicators_config = kwargs.get("resource_indicators", None) self.enable_resource_indicators = kwargs.get("enable_resource_indicators", False) + self.audience_policies_config = kwargs.get("audience_policies", None) + self.enable_audience_policies = kwargs.get("enable_audience_policies", False) def configure_types(self, helpers, default_helpers): if helpers is None: diff --git a/src/idpyoidc/server/oauth2/token_helper/__init__.py b/src/idpyoidc/server/oauth2/token_helper/__init__.py index 42978733..b6efbeeb 100644 --- a/src/idpyoidc/server/oauth2/token_helper/__init__.py +++ b/src/idpyoidc/server/oauth2/token_helper/__init__.py @@ -2,12 +2,14 @@ from typing import Optional from typing import Union +from idpyoidc.exception import ImproperlyConfigured from idpyoidc.message import Message from idpyoidc.message.oidc import TokenErrorResponse from idpyoidc.server.constant import DEFAULT_TOKEN_LIFETIME from idpyoidc.server.session.grant import Grant from idpyoidc.server.session.token import SessionToken from idpyoidc.time_util import utc_time_sans_frac +from idpyoidc.util import importer logger = logging.getLogger(__name__) @@ -187,3 +189,31 @@ def validate_token_exchange_policy(request, context, subject_token, **kwargs): del request["scope"] return request + +def apply_audience_policies(request, context, client_info, audience, grant, configuration): + client_id = request["client_id"] + audience_policies_config = configuration.get("enable_audience_policies", None) + if audience_policies_config is None: + return + audience_policies = configuration.get("audience_policies", None) + if client_id in audience_policies: + applied_audience_policies = audience_policies[client_id] + elif "" in audience_policies: + applied_audience_policies = audience_policies[""] + for audience_policy in applied_audience_policies: + function = audience_policy["function"] + kwargs = audience_policy.get("kwargs", {}) + if isinstance(function, str): + try: + fn = importer(function) + except Exception: + raise ImproperlyConfigured(f"Error importing {function} audience function") + else: + fn = function + try: + fn(request, context, client_info, audience, grant, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} audience function: {e}") + request["error"] = "server_error" + request["error_description"] = "Internal server error" + return diff --git a/src/idpyoidc/server/oauth2/token_helper/access_token.py b/src/idpyoidc/server/oauth2/token_helper/access_token.py index 2af9b44e..9198692c 100755 --- a/src/idpyoidc/server/oauth2/token_helper/access_token.py +++ b/src/idpyoidc/server/oauth2/token_helper/access_token.py @@ -8,6 +8,7 @@ from idpyoidc.exception import ImproperlyConfigured from idpyoidc.message import Message from idpyoidc.message.oauth2 import TokenErrorResponse +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.util import sanitize from ...session import MintingNotAllowed @@ -135,7 +136,12 @@ def process_request(self, req: Union[Message, dict], **kwargs): "scope": scope, } - if "access_token" in _supports_minting: + apply_audience_policies(req, _context, _cinfo, req.get("resource", None), grant, self.endpoint.kwargs) + if "error" in req: + return TokenErrorResponse(error=req["error"], error_description=req["error_description"]) + + if "access_token" in _supports_minting: + resources = req.get("resource", None) if resources and resource_indicators_config is not None: token_args = {"resources": resources} diff --git a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py index 0fb40d05..ebb6ab79 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -6,6 +6,7 @@ from idpyoidc.message import Message from idpyoidc.message.oauth2 import TokenErrorResponse, AuthorizationErrorResponse from idpyoidc.message.oauth2 import CCAccessTokenRequest +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.time_util import utc_time_sans_frac from idpyoidc.util import importer from idpyoidc.util import sanitize @@ -97,19 +98,33 @@ def process_request(self, req: Union[Message, dict], **kwargs): if resources: token_args = {"resources": resources} + apply_audience_policies(req, _context, _cinfo, req.get("resource", None), _session_info["grant"], self.endpoint.kwargs) + if "error" in req: + return self.error_cls(error=req["error"], error_description=req["error_description"]) + resources = req.get("resource", None) + if resources: + token_args = {"resources": resources} _grant = _session_info["grant"] token_type = "Bearer" - _allowed = _context.cdb[client_id].get("allowed_scopes", []) + scopes_allowed_cfg = _context.cdb[client_id].get("allowed_scopes", []) + scopes_req = req.get("scope") or [] + scopes = [ + scope + for scope in scopes_req + if scope in scopes_allowed_cfg + ] + self._apply_client_credentials_filter_policy(req, _grant) + access_token = self._mint_token( token_class="access_token", grant=_grant, session_id=_session_info["branch_id"], client_id=_session_info["client_id"], based_on=None, - scope=_allowed, + scope=scopes, token_type=token_type, token_args=token_args, ) @@ -117,7 +132,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): _resp = { "access_token": access_token.value, "token_type": access_token.token_class, - "scope": _allowed, + "scope": scopes, } if access_token.expires_at: diff --git a/src/idpyoidc/server/oauth2/token_helper/token_exchange.py b/src/idpyoidc/server/oauth2/token_helper/token_exchange.py index bb9a9a55..3b185d5c 100755 --- a/src/idpyoidc/server/oauth2/token_helper/token_exchange.py +++ b/src/idpyoidc/server/oauth2/token_helper/token_exchange.py @@ -13,6 +13,7 @@ from idpyoidc.server.exception import ToOld from idpyoidc.server.exception import UnAuthorizedClientScope from idpyoidc.server.oauth2.authorization import check_unknown_scopes_policy +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.server.session.token import TOKEN_TYPES_MAPPING from idpyoidc.server.session.token import MintingNotAllowed from idpyoidc.server.token.exception import UnknownToken @@ -314,6 +315,13 @@ def process_request(self, request, **kwargs): requested_resources = request.get("resource") or [] requested_aud = request.get("audience") or [] resources = list(set(requested_resources + requested_aud)) + apply_audience_policies(request, _context, _cinfo, request.get("resource", None), grant, self.endpoint.kwargs, **kwargs) + if "error" in request: + return TokenErrorResponse(error=request["error"], error_description=request["error_description"]) + + resources = request.get("resource", None) + if resources: + _token_args = {"resources": resources} try: new_token = self._mint_token( diff --git a/src/idpyoidc/server/oidc/token_helper/access_token.py b/src/idpyoidc/server/oidc/token_helper/access_token.py index 6f2668d9..63b8cdde 100755 --- a/src/idpyoidc/server/oidc/token_helper/access_token.py +++ b/src/idpyoidc/server/oidc/token_helper/access_token.py @@ -10,6 +10,7 @@ from idpyoidc.exception import ImproperlyConfigured from idpyoidc.message import Message from idpyoidc.message.oauth2 import TokenErrorResponse +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.server.oauth2.token_helper import TokenEndpointHelper from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.session.token import AuthorizationCode @@ -150,6 +151,12 @@ def process_request(self, req: Union[Message, dict], **kwargs): if isinstance(req, TokenErrorResponse): return req + _cinfo = self.endpoint.upstream_get("context").cdb.get(client_id) + apply_audience_policies(req, _context, _cinfo, req.get("resource", None), grant, self.endpoint.kwargs) + if "error" in req: + return self.error_cls( + error=req["error"], error_description=req["error_description"] + ) # Maybe there is a different resource at the request from auth code. # We must take it into account diff --git a/src/idpyoidc/server/oidc/token_helper/refresh_token.py b/src/idpyoidc/server/oidc/token_helper/refresh_token.py index 64b9cef0..a9406a66 100755 --- a/src/idpyoidc/server/oidc/token_helper/refresh_token.py +++ b/src/idpyoidc/server/oidc/token_helper/refresh_token.py @@ -10,7 +10,8 @@ from ...exception import InvalidBranchID from idpyoidc.exception import MissingRequiredAttribute from idpyoidc.message import Message -from idpyoidc.message.oidc import RefreshAccessTokenRequest +from idpyoidc.message.oidc import RefreshAccessTokenRequest, AuthorizationResponse +from idpyoidc.server.oauth2.token_helper import apply_audience_policies from idpyoidc.server.oauth2.token_helper import TokenEndpointHelper from idpyoidc.server.session.token import AuthorizationCode from idpyoidc.server.session.token import MintingNotAllowed @@ -54,6 +55,12 @@ def process_request(self, req: Union[Message, dict], **kwargs): scope = _grant.find_scope(token.based_on) if "scope" in req: scope = req["scope"] + + _cinfo = _context.cdb.get(_session_info["client_id"]) + apply_audience_policies(req, _context, _cinfo, _grant.resources, _grant, self.endpoint.kwargs) + if "error" in req: + return self.error_cls(error=req["error"], error_description=req["error_description"]) + access_token = self._mint_token( token_class="access_token", grant=_grant, From aa5d3ba3f1cf8e071de90c7f4e4f0c9a333eb05f Mon Sep 17 00:00:00 2001 From: Nick Mastoris Date: Mon, 24 Feb 2025 17:30:10 +0000 Subject: [PATCH 10/17] Fix broken tests of idpy-oidc --- requirements-dev.txt | 8 +- .../oauth2/token_helper/client_credentials.py | 5 +- ...st_server_24_oauth2_resource_indicators.py | 105 +++++++++--------- tests/test_server_24_oauth2_token_endpoint.py | 22 ++-- tests/test_server_31_oauth2_introspection.py | 6 +- tests/test_server_36_oauth2_token_exchange.py | 46 ++++---- tests/test_tandem_oauth2_token_exchange.py | 2 +- tests/test_tandem_oauth2_token_revocation.py | 13 ++- 8 files changed, 114 insertions(+), 93 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c518bd6..93453ce2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,10 @@ pytest-isort>=1.3.0 pytest-localserver>=0.5.0 flake8 bandit -urllib3<1.27 \ No newline at end of file +urllib3<1.27 +cryptojwt>=1.8.4 +pyOpenSSL +filelock>=3.0.12 +pyyaml>=5.1.2 +jinja2>=2.11.3 +responses>=0.13.0 diff --git a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py index ebb6ab79..c86147ff 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -169,8 +169,9 @@ def _enforce_resource_indicators_policy(self, request, config): def _apply_client_credentials_filter_policy(self, request, grant): _context = self.endpoint.upstream_get("context") - - policy = self.config["policy"] + policy = self.config.get("policy") + if not policy: + return function = policy[""]["function"] kwargs = policy.get("kwargs", {}) if isinstance(function, str): diff --git a/tests/test_server_24_oauth2_resource_indicators.py b/tests/test_server_24_oauth2_resource_indicators.py index 14f66676..41b8a667 100644 --- a/tests/test_server_24_oauth2_resource_indicators.py +++ b/tests/test_server_24_oauth2_resource_indicators.py @@ -37,14 +37,9 @@ from idpyoidc.server.oauth2.authorization import get_uri from idpyoidc.server.oauth2.authorization import inputs from idpyoidc.server.oauth2.authorization import join_query -from idpyoidc.server.oauth2.authorization import ( - validate_resource_indicators_policy as validate_authorization_resource_indicators_policy, -) +from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.oauth2.authorization import verify_uri from idpyoidc.server.oauth2.token import Token -from idpyoidc.server.oauth2.token_helper import ( - validate_resource_indicators_policy as validate_token_resource_indicators_policy, -) from idpyoidc.server.user_info import UserInfo from idpyoidc.time_util import in_a_while from tests import CRYPT_CONFIG @@ -326,12 +321,12 @@ def get_cookie_value(cookie=None, name=None): "path": "{}/authorization", "class": Authorization, "kwargs": { + "enable_resource_indicators": True, "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], "claims_parameter_supported": True, "request_parameter_supported": True, "request_uri_parameter_supported": True, - "resource_indicators_supported": True, }, }, "token": { @@ -543,7 +538,7 @@ def test_authorization_code_req_per_client(self, create_endpoint_ri_disabled): endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_3"]}, }, } @@ -576,7 +571,7 @@ def test_authorization_code_resource_indicators_enabled_resource_exists(self, cr endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -595,7 +590,7 @@ def test_authorization_code_resource_indicators_enabled_resource_itself(self, cr endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -629,13 +624,13 @@ def test_authorization_code_resource_indicators_enabled_resource_unknown(self, c endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) assert "error" in msg - assert msg["error_description"] == f"Invalid resource requested by client {client_id}" + assert msg["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_authorization_code_resource_indicators_disabled_resource_multiple(self, create_endpoint_ri_disabled): """ @@ -649,7 +644,7 @@ def test_authorization_code_resource_indicators_disabled_resource_multiple(self, endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -658,7 +653,7 @@ def test_authorization_code_resource_indicators_disabled_resource_multiple(self, def test_authorization_code_resource_indicators_enabled_resource_multiple_with_itself(self, create_endpoint_ri_enabled): """ - Test that no error message is returned when resource indicators is enabled + Test that error message is returned when resource indicators is enabled there is no client configuration for resource_indicators and requested resource contains the client itself, among others that are unknown. """ @@ -668,7 +663,8 @@ def test_authorization_code_resource_indicators_enabled_resource_multiple_with_i endpoint_context = self.endpoint.upstream_get("context") msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) - assert "error" not in msg + assert "error" in msg + assert msg["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_authorization_code_resource_indicators_enabled_resource_multiple_unknown(self, create_endpoint_ri_enabled): """ @@ -682,13 +678,13 @@ def test_authorization_code_resource_indicators_enabled_resource_multiple_unknow endpoint_context = self.endpoint.upstream_get("context") endpoint_context.cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) assert "error" in msg - assert msg["error_description"] == f"Invalid resource requested by client {client_id}" + assert msg["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_access_token_req_disabled_resource_multiple_resource_no_client_conf(self, create_endpoint_ri_enabled): """ @@ -729,7 +725,7 @@ def test_access_token_req_disabled_resource_multiple_resource_client_conf(self, self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -768,7 +764,7 @@ def test_access_token_req_resource_known(self, create_endpoint_ri_enabled): } self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -806,7 +802,7 @@ def test_access_token_req_resource_itself(self, create_endpoint_ri_enabled): } self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -874,7 +870,7 @@ def test_access_token_req_invalid_multiple_resource_client_conf(self, create_end self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -894,8 +890,8 @@ def test_access_token_req_invalid_multiple_resource_client_conf(self, create_end assert "error" in _resp assert _resp["error"] == "invalid_target" - assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" - + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" + def test_access_token_req_disabled_ri_multiple_resource_client_conf(self, create_endpoint_ri_disabled): """ Test that no error message is returned (neither an "aud" claim) @@ -905,7 +901,7 @@ def test_access_token_req_disabled_ri_multiple_resource_client_conf(self, create self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -938,7 +934,7 @@ def test_access_token_req_resource_itself_multiple_resource_one_known(self, crea self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -957,29 +953,27 @@ def test_access_token_req_resource_itself_multiple_resource_one_known(self, crea assert code.resources != [] _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] _token_request["code"] = code.value _token_request["resource"] = ["client_2", "client_3"] _req = self.token_endpoint.parse_request(_token_request) _resp = self.token_endpoint.process_request(request=_req) - access_token = TokenErrorResponse().from_jwt( - _resp["response_args"]["access_token"], - self.endpoint_context.keyjar, - sender="", - ) - - assert "client_2" in access_token["aud"] + assert "error" in _resp + assert _resp["error"] == "invalid_target" + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_access_token_req_multiple_resource_itself(self, create_endpoint_ri_enabled): """ - Test successful access_token request when resource indicators is enabled. - and the request contains a resource about client itself, among other unknown clients + Test that error message is returned + when resource indicators is enabled + and the request contains at least one unknown client """ self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -998,25 +992,22 @@ def test_access_token_req_multiple_resource_itself(self, create_endpoint_ri_enab assert code.resources != [] _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] _token_request["code"] = code.value _token_request["resource"] = ["client_1", "client_3"] _req = self.token_endpoint.parse_request(_token_request) _resp = self.token_endpoint.process_request(request=_req) - access_token = TokenErrorResponse().from_jwt( - _resp["response_args"]["access_token"], - self.endpoint_context.keyjar, - sender="", - ) - - assert "client_1" in access_token["aud"] + assert "error" in _resp + assert _resp["error"] == "invalid_target" + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_access_token_req_multiple_resource_itself_no_client_conf(self, create_endpoint_ri_enabled): """ - Test successful access_token request when resource indicators is enabled, + Test error message is returned when resource indicators is enabled, there is no client configuration for resource indicators - and the request contains a resource about client itself, among other unknown clients + and the request contains a resource """ self.endpoint.upstream_get("context").cdb["client_3"] = { @@ -1033,19 +1024,16 @@ def test_access_token_req_multiple_resource_itself_no_client_conf(self, create_e assert code.resources != [] _token_request = TOKEN_REQ_DICT.copy() + client_id = _token_request["client_id"] _token_request["code"] = code.value _token_request["resource"] = ["client_1", "client_3"] _req = self.token_endpoint.parse_request(_token_request) _resp = self.token_endpoint.process_request(request=_req) - access_token = TokenErrorResponse().from_jwt( - _resp["response_args"]["access_token"], - self.endpoint_context.keyjar, - sender="", - ) - - assert "client_1" in access_token["aud"] + assert "error" in _resp + assert _resp["error"] == "invalid_target" + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_access_token_req_multiple_resource_known(self, create_endpoint_ri_enabled): """ @@ -1060,7 +1048,7 @@ def test_access_token_req_multiple_resource_known(self, create_endpoint_ri_enabl } self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2","client_3"]}, }, } @@ -1093,7 +1081,7 @@ def test_access_token_req_invalid_multiple_resource_unknown(self, create_endpoin """ self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { "policy": { - "function": validate_authorization_resource_indicators_policy, + "function": validate_resource_indicators_policy, "kwargs": {"resource_servers_per_client": ["client_2"]}, }, } @@ -1114,13 +1102,20 @@ def test_access_token_req_invalid_multiple_resource_unknown(self, create_endpoin assert "error" in _resp assert _resp["error"] == "invalid_target" - assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_create_authn_response(self, create_endpoint_ri_enabled): """ Test that the requested access_token has the correct scopes based on the allowed scopes of the requested resources """ + self.endpoint.upstream_get("context").cdb["client_1"]["resource_indicators"] = { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_3"]}, + }, + } + self.endpoint.upstream_get("context").cdb["client_3"] = { "client_id": "client_3", "redirect_uris": [("https://rp.example.com/cb", {})], @@ -1141,4 +1136,4 @@ def test_create_authn_response(self, create_endpoint_ri_enabled): _resp = self.token_endpoint.process_request(request=_req) assert "response_args" in _resp - assert set(_resp["response_args"]["scope"]) == set(["openid", "profile"]) + assert set(_resp["response_args"]["scope"]) == set(["openid"]) diff --git a/tests/test_server_24_oauth2_token_endpoint.py b/tests/test_server_24_oauth2_token_endpoint.py index 889c67e5..d8884b4e 100644 --- a/tests/test_server_24_oauth2_token_endpoint.py +++ b/tests/test_server_24_oauth2_token_endpoint.py @@ -1078,7 +1078,7 @@ def test_client_credentials_resource_indicator_enabled_client_conf_unknown_resou response = self.token_endpoint.process_request(request) assert response["error"] == "invalid_target" - assert response["error_description"] == f"Invalid resource requested by client {client_id}" + assert response["error_description"] == f"One or more invalid resources requested by client {client_id}" @pytest.mark.parametrize("resource", ["client_1", ["client_1", "client_3"]]) def test_client_credentials_resource_indicator_enabled_client_conf_itself_resource(self, resource): @@ -1097,17 +1097,19 @@ def test_client_credentials_resource_indicator_enabled_client_conf_itself_resour scope="whatever", resource=resource ) - + client_id = request["client_id"] request = self.token_endpoint.parse_request(request) response = self.token_endpoint.process_request(request) - # Access Token - access_token = AuthorizationResponse().from_jwt( - response["response_args"]["access_token"], self.keyjar, sender="" - ) - - assert "aud" in access_token - assert "client_1" in access_token["aud"] - + if resource == "client_1": + # Access Token + access_token = AuthorizationResponse().from_jwt( + response["response_args"]["access_token"], self.keyjar, sender="" + ) + assert "aud" in access_token + assert "client_1" in access_token["aud"] + else: + assert response["error"] == "invalid_target" + assert response["error_description"] == f"One or more invalid resources requested by client {client_id}" class TestResourceOwnerPasswordCredentialsFlow(object): @pytest.fixture(autouse=True) diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py index bf283476..06f06209 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -70,6 +70,7 @@ scope=["openid"], state="STATE", response_type="code id_token", + resource="client_1", ) TOKEN_REQ = AccessTokenRequest( @@ -242,7 +243,10 @@ def _mint_token(self, token_class, grant, session_id, based_on=None, **kwargs): def _get_access_token(self, areq): session_id = self._create_session(areq) # Consent handling + print("*************authz**************") grant = self.token_endpoint.upstream_get("context").authz(session_id, areq) + print("*************end authz**************") + print(grant.resources) self.session_manager[session_id] = grant # grant = self.session_manager[session_id] code = self._mint_token("authorization_code", grant, session_id) @@ -494,7 +498,7 @@ def test_revoked_access_token(self): def test_wrong_aud(self): auth_req = AUTH_REQ.copy() - auth_req["client_id"] = "client_2" + auth_req["resource"] = "client_2" access_token = self._get_access_token(auth_req) _context = self.introspection_endpoint.upstream_get("endpoint_context") diff --git a/tests/test_server_36_oauth2_token_exchange.py b/tests/test_server_36_oauth2_token_exchange.py index eef68993..7a5eec0b 100644 --- a/tests/test_server_36_oauth2_token_exchange.py +++ b/tests/test_server_36_oauth2_token_exchange.py @@ -16,7 +16,7 @@ from idpyoidc.server.client_authn import verify_client from idpyoidc.server.configure import ASConfiguration from idpyoidc.server.cookie_handler import CookieHandler -from idpyoidc.server.oauth2.authorization import validate_resource_indicators_policy +from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD from idpyoidc.server.user_info import UserInfo from tests import CRYPT_CONFIG @@ -694,7 +694,7 @@ def test_wrong_resource(self, resource): _resp = self.endpoint.process_request(request=_req) assert set(_resp.keys()) == {"error", "error_description"} assert _resp["error"] == "invalid_target" - assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" @pytest.mark.parametrize("resource", ["client_2", ["client_2", "client_3"]]) def test_token_exchange_req_resource(self, resource): @@ -720,8 +720,9 @@ def test_token_exchange_req_resource(self, resource): _token_request = TOKEN_REQ_DICT.copy() _token_request["code"] = code.value _req = self.endpoint.parse_request(_token_request) + client_id = _req["client_id"] _resp = self.endpoint.process_request(request=_req) - + _token_value = _resp["response_args"]["access_token"] token_exchange_req = TokenExchangeRequest( @@ -734,25 +735,27 @@ def test_token_exchange_req_resource(self, resource): _req = self.endpoint.parse_request( token_exchange_req, {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, - ) + ) _resp = self.endpoint.process_request(request=_req) - - assert set(_resp["response_args"].keys()) == { - "access_token", - "token_type", - "expires_in", - "issued_token_type", - "scope", - } - msg = self.endpoint.do_response(request=_req, **_resp) - assert isinstance(msg, dict) - - id_token = AuthorizationResponse().from_jwt( - _resp["response_args"]["access_token"], self.keyjar, sender="" - ) - - assert "client_2" in id_token["aud"] - assert "client_3" not in id_token["aud"] + if resource == "client_2": + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "issued_token_type", + "scope", + } + msg = self.endpoint.do_response(request=_req, **_resp) + assert isinstance(msg, dict) + + id_token = AuthorizationResponse().from_jwt( + _resp["response_args"]["access_token"], self.keyjar, sender="" + ) + + assert "client_2" in id_token["aud"] + else: + assert _resp["error"] == "invalid_target" + assert _resp["error_description"] == f"One or more invalid resources requested by client {client_id}" def test_refresh_token_audience(self): @@ -1336,6 +1339,7 @@ def test_token_exchange_unsupported_scope_requested_3(self): assert _resp["response_args"]["scope"] == ["profile"] token_exchange_req["scope"] = "offline_access" + token_exchange_req["resource"] = "client_1" _req = self.endpoint.parse_request( token_exchange_req.to_urlencoded(), diff --git a/tests/test_tandem_oauth2_token_exchange.py b/tests/test_tandem_oauth2_token_exchange.py index 1d6c0645..556a18c0 100644 --- a/tests/test_tandem_oauth2_token_exchange.py +++ b/tests/test_tandem_oauth2_token_exchange.py @@ -477,7 +477,7 @@ def test_wrong_resource(self): assert set(_te_resp.keys()) == {"error", "error_description"} assert _te_resp["error"] == "invalid_target" - assert _te_resp["error_description"] == "Invalid resource requested by client client_1" + assert _te_resp["error_description"] == "One or more invalid resources requested by client client_1" def test_refresh_token_audience(self): """ diff --git a/tests/test_tandem_oauth2_token_revocation.py b/tests/test_tandem_oauth2_token_revocation.py index 92d44309..5c3da473 100644 --- a/tests/test_tandem_oauth2_token_revocation.py +++ b/tests/test_tandem_oauth2_token_revocation.py @@ -1,5 +1,4 @@ import os - import pytest from cryptojwt.key_jar import build_keyjar @@ -9,6 +8,7 @@ from idpyoidc.server import Server from idpyoidc.server.authz import AuthzHandling from idpyoidc.server.client_authn import verify_client +from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD from idpyoidc.server.user_info import UserInfo from idpyoidc.util import rndstr @@ -46,7 +46,9 @@ def create_entities(self): "token": { "path": "token", "class": "idpyoidc.server.oauth2.token.Token", - "kwargs": {}, + "kwargs": { + "enable_resource_indicators": True, + }, }, "token_revocation": { "path": "revocation", @@ -122,6 +124,12 @@ def create_entities(self): "client_secret": "abcdefghijklmnop", "issuer": "https://example.com/", "response_types_supported": ["code"], + "resource_indicators": { + "policy": { + "function": validate_resource_indicators_policy, + "kwargs": {"resource_servers_per_client": ["client_1"]}, + }, + }, } services = { "server_metadata": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"}, @@ -205,6 +213,7 @@ def process_setup(self, token=None, scope=None): "code": auth_response["code"], "state": auth_response["state"], "redirect_uri": areq["redirect_uri"], + "resource": "client_1", # "grant_type": "authorization_code", # "client_id": self.client_.get_client_id(), # "client_secret": _context.get_usage("client_secret"), From c26d43a8cfb8e6390ec362c53bd624256601ec59 Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Fri, 14 Mar 2025 06:35:26 +0000 Subject: [PATCH 11/17] Fix broken tests of idpy-oidc --- src/idpyoidc/server/oauth2/introspection.py | 8 ++++---- tests/test_server_31_oauth2_introspection.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/idpyoidc/server/oauth2/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index 4a3bd600..6b6bcaa4 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -132,13 +132,13 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs ) except: enforce_aud_restriction = self.enforce_aud_restriction - if enforce_aud_restriction: - if request["client_id"] not in aud: - return {"response_args": _resp} - + _info = self._introspect(_token, _session_info["client_id"], grant) if _info is None: return {"response_args": _resp} + if enforce_aud_restriction: + if request["client_id"] not in aud and request["client_id"] not in _info["client_id"]: + return {"response_args": _resp} if release: if "username" in release: diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py index 06f06209..e7c9ae66 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -342,7 +342,6 @@ def test_do_response(self): "exp", "iat", "scope", - "aud", "token_type", } assert _payload["active"] is True From 09fb5ab11b3710b7c6d885cbb364807a9cfc8c90 Mon Sep 17 00:00:00 2001 From: Ivan Kanakarakis Date: Tue, 1 Apr 2025 07:59:17 +0000 Subject: [PATCH 12/17] Add client_id to the audience policy interface --- .../server/oauth2/token_helper/__init__.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/idpyoidc/server/oauth2/token_helper/__init__.py b/src/idpyoidc/server/oauth2/token_helper/__init__.py index b6efbeeb..d1ceb2c0 100644 --- a/src/idpyoidc/server/oauth2/token_helper/__init__.py +++ b/src/idpyoidc/server/oauth2/token_helper/__init__.py @@ -106,7 +106,7 @@ def validate_resource_indicators_policy(request, context, **kwargs): if isinstance(request["resource"], str): # If it's a string, convert it to a list request["resource"] = [request["resource"]] - + permitted_resources = [res for res in resource_servers_per_client] if client_id not in permitted_resources: permitted_resources.append(client_id) @@ -159,7 +159,7 @@ def validate_token_exchange_policy(request, context, subject_token, **kwargs): return TokenErrorResponse( error="invalid_target", error_description="Refresh token has single owner" ) - audience = kwargs.get("audience", []) + audience = kwargs.get("audience") or [] if audience and not set(request["audience"]).issubset(set(audience)): return TokenErrorResponse(error="invalid_target", error_description="Unknown audience") @@ -191,18 +191,31 @@ def validate_token_exchange_policy(request, context, subject_token, **kwargs): return request def apply_audience_policies(request, context, client_info, audience, grant, configuration): + """ + request (Message): the request being processed + context (dict): context + client_id (str): the id of the client making the request + client_info (dict): more information about the client + audience (list): the intended audience of the token; if the request is ClientCredentials or AuthorizationCode then this is the requested resources through Resource Indicators RFC + grant: the associated grant with the token + configuration (dict): extra configuration for the policy + """ + client_id = request["client_id"] audience_policies_config = configuration.get("enable_audience_policies", None) if audience_policies_config is None: return - audience_policies = configuration.get("audience_policies", None) - if client_id in audience_policies: - applied_audience_policies = audience_policies[client_id] - elif "" in audience_policies: - applied_audience_policies = audience_policies[""] + + audience_policies = configuration.get("audience_policies") or {} + applied_audience_policies = ( + audience_policies.get(client_id) + or audience_policies.get("") + or [] + ) for audience_policy in applied_audience_policies: function = audience_policy["function"] kwargs = audience_policy.get("kwargs", {}) + if isinstance(function, str): try: fn = importer(function) @@ -210,8 +223,9 @@ def apply_audience_policies(request, context, client_info, audience, grant, conf raise ImproperlyConfigured(f"Error importing {function} audience function") else: fn = function + try: - fn(request, context, client_info, audience, grant, **kwargs) + fn(request, context, client_id, client_info, audience, grant, **kwargs) except Exception as e: logger.error(f"Error while executing the {fn} audience function: {e}") request["error"] = "server_error" From 81fdc0c7a17ed72bb7adfdd3f820bedd1b4c659b Mon Sep 17 00:00:00 2001 From: Nick Mastoris Date: Tue, 26 Aug 2025 09:22:30 +0000 Subject: [PATCH 13/17] Handle explicit None for add_claims --- src/idpyoidc/server/session/claims.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/idpyoidc/server/session/claims.py b/src/idpyoidc/server/session/claims.py index 272b4636..f249c218 100755 --- a/src/idpyoidc/server/session/claims.py +++ b/src/idpyoidc/server/session/claims.py @@ -69,7 +69,9 @@ def _client_claims( secondary_identifier: Optional[str] = "", ): _cdb = self.upstream_get("attribute", "cdb") - add_claims_by_scope = _cdb[client_id].get("add_claims", {}).get("by_scope", {}) + add_claims = _cdb[client_id].get("add_claims") or {} + add_claims_by_scope = add_claims.get("by_scope") or {} + if add_claims_by_scope: _claims_by_scope = add_claims_by_scope.get(claims_release_point) if _claims_by_scope is None and secondary_identifier: From 15145c3a34a33bb70021393e8b34d02111e0ca0b Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Thu, 4 Sep 2025 15:53:19 +0000 Subject: [PATCH 14/17] Handle "None" value for the always part of the config --- src/idpyoidc/server/session/claims.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idpyoidc/server/session/claims.py b/src/idpyoidc/server/session/claims.py index f249c218..27aca173 100755 --- a/src/idpyoidc/server/session/claims.py +++ b/src/idpyoidc/server/session/claims.py @@ -82,7 +82,7 @@ def _client_claims( else: _claims_by_scope = module.kwargs.get("add_claims_by_scope", {}) - add_claims_always = _cdb[client_id].get("add_claims", {}).get("always", {}) + add_claims_always = add_claims.get("always") or {} _always_add = add_claims_always.get(claims_release_point, []) if secondary_identifier: _always_2 = add_claims_always.get(secondary_identifier, []) From bd8195297f3f15511e95acd5604a0fb5b208e81c Mon Sep 17 00:00:00 2001 From: Nick Mastoris Date: Mon, 10 Feb 2025 09:54:58 +0000 Subject: [PATCH 15/17] we need client_id --- src/idpyoidc/server/token/id_token.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/idpyoidc/server/token/id_token.py b/src/idpyoidc/server/token/id_token.py index 3f101275..646ea6ef 100755 --- a/src/idpyoidc/server/token/id_token.py +++ b/src/idpyoidc/server/token/id_token.py @@ -283,10 +283,10 @@ def __call__( ) -> str: _context = self.upstream_get("context") - try: - del kwargs["client_id"] - except KeyError: - pass + # try: + # del kwargs["client_id"] + # except KeyError: + # pass user_id, client_id, grant_id = _context.session_manager.decrypt_session_id(session_id) From 099f251b277fb6b090dc6cd5370a7bc805e59ec1 Mon Sep 17 00:00:00 2001 From: Nick Mastoris Date: Mon, 31 Mar 2025 07:14:24 +0000 Subject: [PATCH 16/17] keep client_id at id_token, change test for introspection as we chage the logic for aud enforce restriction --- src/idpyoidc/server/oauth2/authorization.py | 2 +- src/idpyoidc/server/oauth2/introspection.py | 4 ++-- src/idpyoidc/server/oauth2/token_helper/__init__.py | 3 +-- tests/test_server_08_id_token.py | 7 +++++++ tests/test_server_31_oauth2_introspection.py | 5 +---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index b5a7e7fd..02ec0e4a 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -339,6 +339,7 @@ def check_unknown_scopes_policy(request_info, client_id, context): logger.warning(f"{client_id} requested unauthorized scopes: {diff}") raise UnAuthorizedClientScope() + class Authorization(Endpoint): request_cls = oauth2.AuthorizationRequest response_cls = oauth2.AuthorizationResponse @@ -564,7 +565,6 @@ def _post_parse_request(self, request, client_id, context, **kwargs): if "policy" not in resource_indicators_config: policy = {"policy": {"function": validate_resource_indicators_policy}} resource_indicators_config.update(policy) - request = self._enforce_resource_indicators_policy(request, resource_indicators_config) if "error" in request: diff --git a/src/idpyoidc/server/oauth2/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index 6b6bcaa4..7ff00f9f 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -120,7 +120,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs aud = grant.resources client_id = request["client_id"] - + apply_audience_policies(request, _context, _context.cdb[client_id], aud, _session_info["grant"], self.kwargs) if "error" in request: return {"response_args": _resp} @@ -132,7 +132,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs ) except: enforce_aud_restriction = self.enforce_aud_restriction - + _info = self._introspect(_token, _session_info["client_id"], grant) if _info is None: return {"response_args": _resp} diff --git a/src/idpyoidc/server/oauth2/token_helper/__init__.py b/src/idpyoidc/server/oauth2/token_helper/__init__.py index d1ceb2c0..598844f0 100644 --- a/src/idpyoidc/server/oauth2/token_helper/__init__.py +++ b/src/idpyoidc/server/oauth2/token_helper/__init__.py @@ -93,7 +93,6 @@ def validate_resource_indicators_policy(request, context, **kwargs): client_id = request["client_id"] resource_servers_per_client = kwargs.get("resource_servers_per_client", []) - if ( isinstance(resource_servers_per_client, dict) and client_id not in resource_servers_per_client @@ -202,7 +201,7 @@ def apply_audience_policies(request, context, client_info, audience, grant, conf """ client_id = request["client_id"] - audience_policies_config = configuration.get("enable_audience_policies", None) + audience_policies_config = configuration.get("enable_audience_policies", None) if configuration else None if audience_policies_config is None: return diff --git a/tests/test_server_08_id_token.py b/tests/test_server_08_id_token.py index ecc72c68..37afe559 100644 --- a/tests/test_server_08_id_token.py +++ b/tests/test_server_08_id_token.py @@ -235,6 +235,7 @@ def test_id_token_payload_0(self): payload = _jwt.jwt.payload() assert set(payload.keys()) == { "aud", + "client_id", "sub", "auth_time", "nonce", @@ -285,6 +286,7 @@ def test_id_token_lifetime_per_client(self): assert set(payload.keys()) == { "aud", + "client_id", "sub", "auth_time", "nonce", @@ -310,6 +312,7 @@ def test_id_token_payload_with_code(self): payload = _jwt.jwt.payload() assert set(payload.keys()) == { "sub", + "client_id", "auth_time", "aud", "exp", @@ -343,6 +346,7 @@ def test_id_token_payload_with_access_token(self): assert set(payload.keys()) == { "sub", + "client_id", "auth_time", "aud", "exp", @@ -378,6 +382,7 @@ def test_id_token_payload_with_code_and_access_token(self): payload = _jwt.jwt.payload() assert set(payload.keys()) == { "sub", + "client_id", "auth_time", "aud", "exp", @@ -405,6 +410,7 @@ def test_id_token_payload_with_userinfo(self): payload = _jwt.jwt.payload() assert set(payload.keys()) == { "nonce", + "client_id", "iat", "iss", "email", @@ -441,6 +447,7 @@ def test_id_token_payload_many_0(self): payload = _jwt.jwt.payload() assert set(payload.keys()) == { "nonce", + "client_id", "c_hash", "at_hash", "email", diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py index e7c9ae66..f5151a20 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -70,7 +70,6 @@ scope=["openid"], state="STATE", response_type="code id_token", - resource="client_1", ) TOKEN_REQ = AccessTokenRequest( @@ -243,10 +242,7 @@ def _mint_token(self, token_class, grant, session_id, based_on=None, **kwargs): def _get_access_token(self, areq): session_id = self._create_session(areq) # Consent handling - print("*************authz**************") grant = self.token_endpoint.upstream_get("context").authz(session_id, areq) - print("*************end authz**************") - print(grant.resources) self.session_manager[session_id] = grant # grant = self.session_manager[session_id] code = self._mint_token("authorization_code", grant, session_id) @@ -497,6 +493,7 @@ def test_revoked_access_token(self): def test_wrong_aud(self): auth_req = AUTH_REQ.copy() + auth_req["client_id"] = "client_3" auth_req["resource"] = "client_2" access_token = self._get_access_token(auth_req) _context = self.introspection_endpoint.upstream_get("endpoint_context") From 2bd72f96b730c766bc7041194c186d364772a493 Mon Sep 17 00:00:00 2001 From: Nick Mastoris Date: Tue, 11 Feb 2025 07:45:36 +0000 Subject: [PATCH 17/17] add fix --- src/idpyoidc/server/endpoint.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index 07c0080b..9beb47b3 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -275,11 +275,10 @@ def client_authentication(self, request: Message, http_info: Optional[dict] = No authn_info = verify_client(request=request, http_info=http_info, **kwargs) LOGGER.debug("authn_info: %s", authn_info) - if authn_info == {}: - if self.client_authn_method and len(self.client_authn_method): - LOGGER.debug("client_authn_method: %s", self.client_authn_method) - raise UnAuthorizedClient("Authorization failed") - elif "client_id" not in authn_info and authn_info.get("method") != "none": + if authn_info == {} and self.client_authn_method and len(self.client_authn_method): + LOGGER.debug("client_authn_method: %s", self.client_authn_method) + raise UnAuthorizedClient("Authorization failed") + if "client_id" not in authn_info and authn_info.get("method") != "none": raise UnAuthorizedClient("Authorization failed") return authn_info