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/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/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 diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index e2cd4fa7..02ec0e4a 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,8 @@ 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 from idpyoidc.server.user_authn.authn_context import pick_auth @@ -103,7 +105,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 +178,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 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) @@ -205,6 +210,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 +248,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 +260,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" @@ -331,53 +340,6 @@ def check_unknown_scopes_policy(request_info, client_id, context): 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 @@ -412,6 +374,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 +519,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, @@ -566,19 +529,50 @@ 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 @@ -589,9 +583,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) @@ -931,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 1a7f19c9..7ff00f9f 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( @@ -124,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"], _session_info["grant"]) + _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: @@ -142,6 +150,10 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs _resp.update(_info) _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.py b/src/idpyoidc/server/oauth2/token.py index 30d34fb5..db582358 100755 --- a/src/idpyoidc/server/oauth2/token.py +++ b/src/idpyoidc/server/oauth2/token.py @@ -59,6 +59,9 @@ 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) + 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 43c2a6ca..598844f0 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__) @@ -15,7 +17,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( @@ -86,15 +88,11 @@ 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,50 +101,64 @@ 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( 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") @@ -176,3 +188,45 @@ 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): + """ + 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 configuration else None + if audience_policies_config is None: + return + + 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) + except Exception: + raise ImproperlyConfigured(f"Error importing {function} audience function") + else: + fn = function + + try: + 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" + 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 a5c94c2c..9198692c 100755 --- a/src/idpyoidc/server/oauth2/token_helper/access_token.py +++ b/src/idpyoidc/server/oauth2/token_helper/access_token.py @@ -8,13 +8,14 @@ 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 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 +50,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 +136,22 @@ def process_request(self, req: Union[Message, dict], **kwargs): "scope": scope, } + 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: + 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 fa3db7cd..c86147ff 100755 --- a/src/idpyoidc/server/oauth2/token_helper/client_credentials.py +++ b/src/idpyoidc/server/oauth2/token_helper/client_credentials.py @@ -2,12 +2,17 @@ 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.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 from . import TokenEndpointHelper +from . import validate_resource_indicators_policy logger = logging.getLogger(__name__) @@ -45,25 +50,89 @@ 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} + + 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, ) _resp = { "access_token": access_token.value, "token_type": access_token.token_class, - "scope": _allowed, + "scope": scopes, } if access_token.expires_at: @@ -77,3 +146,43 @@ 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 + 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") diff --git a/src/idpyoidc/server/oauth2/token_helper/token_exchange.py b/src/idpyoidc/server/oauth2/token_helper/token_exchange.py index cc81fb0d..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 @@ -20,6 +21,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 +91,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 +115,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 +174,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,13 +266,60 @@ 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 + # 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)) + 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} diff --git a/src/idpyoidc/server/oidc/token_helper/access_token.py b/src/idpyoidc/server/oidc/token_helper/access_token.py index 2594748e..63b8cdde 100755 --- a/src/idpyoidc/server/oidc/token_helper/access_token.py +++ b/src/idpyoidc/server/oidc/token_helper/access_token.py @@ -5,14 +5,21 @@ 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 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 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 +111,58 @@ 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 + _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 + 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 +171,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 +266,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/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, diff --git a/src/idpyoidc/server/session/claims.py b/src/idpyoidc/server/session/claims.py index 272b4636..27aca173 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: @@ -80,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, []) 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) 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_24_oauth2_resource_indicators.py b/tests/test_server_24_oauth2_resource_indicators.py index 14e6a032..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,41 +321,25 @@ 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": { - "policy": { - "function": validate_authorization_resource_indicators_policy, - "kwargs": { - "resource_servers_per_client": { - "client_1": ["client_1", "client_2"], - }, - }, - } - }, }, }, "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 +532,229 @@ 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_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_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(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_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_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_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"One or more invalid resources 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_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 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" 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): + """ + 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_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}" + assert msg["error_description"] == f"One or more invalid resources 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 successful access_token request when resource indicators is 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_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 + containing also an aud claim with the appropriate client. """ self.endpoint.upstream_get("context").cdb["client_3"] = { "client_id": "client_3", @@ -616,6 +762,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_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 +776,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 +787,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_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) - def test_access_token_req_invalid_resource_client(self, create_endpoint_ri_enabled): + _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_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_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 +882,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) @@ -657,13 +890,232 @@ 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}" + 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) + 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_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_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() + 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) + + 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 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_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() + 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) + + 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 error message is returned when resource indicators is enabled, + there is no client configuration for resource indicators + and the request contains a resource + """ + + 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() + 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) + + 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): + """ + 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_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_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"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", {})], @@ -684,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 4d43d302..d8884b4e 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,7 +968,148 @@ 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"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): + 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) + 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..f5151a20 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -338,7 +338,6 @@ def test_do_response(self): "exp", "iat", "scope", - "aud", "token_type", } assert _payload["active"] is True @@ -494,7 +493,8 @@ def test_revoked_access_token(self): def test_wrong_aud(self): auth_req = AUTH_REQ.copy() - auth_req["client_id"] = "client_2" + 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") diff --git a/tests/test_server_36_oauth2_token_exchange.py b/tests/test_server_36_oauth2_token_exchange.py index 5b3a5663..7a5eec0b 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.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 @@ -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,78 @@ 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"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): + """ + 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) + client_id = _req["client_id"] + _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) + 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): """ @@ -1269,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 6b722d3b..556a18c0 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"] == "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"),