From 2a2e6208f3b739ba46db8b55f136d9716d9645fd Mon Sep 17 00:00:00 2001 From: Kostis Triantafyllakis Date: Tue, 2 Aug 2022 12:56:38 +0300 Subject: [PATCH] Add various fixes --- src/idpyoidc/server/client_authn.py | 60 +- src/idpyoidc/server/endpoint.py | 5 + src/idpyoidc/server/oauth2/introspection.py | 3 +- src/idpyoidc/server/oauth2/token_helper.py | 67 +- src/idpyoidc/server/oidc/session.py | 2 +- src/idpyoidc/server/oidc/token_helper.py | 3 +- src/idpyoidc/server/oidc/userinfo.py | 2 +- src/idpyoidc/server/session/grant.py | 86 ++- src/idpyoidc/server/session/manager.py | 7 +- tests/test_server_17_client_authn.py | 15 +- tests/test_server_20d_client_authn.py | 17 +- ...st_server_23_oidc_registration_endpoint.py | 2 +- tests/test_server_31_oauth2_introspection.py | 9 +- .../test_server_32_oidc_read_registration.py | 2 +- tests/test_server_36_oauth2_token_exchange.py | 675 +++++++++++++++++- tests/test_server_60_dpop.py | 6 +- tests/test_tandem_10_token_exchange.py | 10 +- 17 files changed, 885 insertions(+), 86 deletions(-) diff --git a/src/idpyoidc/server/client_authn.py b/src/idpyoidc/server/client_authn.py index 1c62b556..6e20c3a7 100755 --- a/src/idpyoidc/server/client_authn.py +++ b/src/idpyoidc/server/client_authn.py @@ -262,6 +262,7 @@ def _verify( request: Optional[Union[dict, Message]] = None, authorization_token: Optional[str] = None, endpoint=None, # Optional[Endpoint] + get_client_id_from_token=None, **kwargs, ): _token = request.get("access_token") @@ -269,7 +270,7 @@ def _verify( raise ClientAuthenticationError("No access token") res = {"token": _token} - _client_id = request.get("client_id") + _client_id = get_client_id_from_token(endpoint_context, _token, request) if _client_id: res["client_id"] = _client_id return res @@ -483,6 +484,7 @@ def verify_client( auth_info = {} methods = endpoint_context.client_authn_method + client_id = None allowed_methods = getattr(endpoint, "client_authn_method") if not allowed_methods: allowed_methods = list(methods.keys()) @@ -499,48 +501,48 @@ def verify_client( endpoint=endpoint, get_client_id_from_token=get_client_id_from_token, ) - break except (BearerTokenAuthenticationError, ClientAuthenticationError): raise except Exception as err: logger.info("Verifying auth using {} failed: {}".format(_method.tag, err)) + continue - if auth_info.get("method") == "none": - return auth_info + if auth_info.get("method") == "none" and auth_info.get("client_id") is None: + # For testing purposes only + break - client_id = auth_info.get("client_id") - if client_id is None: - raise ClientAuthenticationError("Failed to verify client") + client_id = auth_info.get("client_id") + if client_id is None: + raise ClientAuthenticationError("Failed to verify client") - if also_known_as: - client_id = also_known_as[client_id] - auth_info["client_id"] = client_id + if also_known_as: + client_id = also_known_as[client_id] + auth_info["client_id"] = client_id - if client_id not in endpoint_context.cdb: - raise UnknownClient("Unknown Client ID") + if client_id not in endpoint_context.cdb: + raise UnknownClient("Unknown Client ID") - _cinfo = endpoint_context.cdb[client_id] + _cinfo = endpoint_context.cdb[client_id] - if not valid_client_info(_cinfo): - logger.warning("Client registration has timed out or " "client secret is expired.") - raise InvalidClient("Not valid client") + if not valid_client_info(_cinfo): + logger.warning("Client registration has timed out or " "client secret is expired.") + raise InvalidClient("Not valid client") - # Validate that the used method is allowed for this client/endpoint - client_allowed_methods = _cinfo.get( - f"{endpoint.endpoint_name}_client_authn_method", _cinfo.get("client_authn_method") - ) - if client_allowed_methods is not None and _method and _method.tag not in client_allowed_methods: - logger.info( - f"Allowed methods for client: {client_id} at endpoint: {endpoint.name} are: " - f"`{', '.join(client_allowed_methods)}`" - ) - raise UnAuthorizedClient( - f"Authentication method: {_method.tag} not allowed for client: {client_id} in " - f"endpoint: {endpoint.name}" + # Validate that the used method is allowed for this client/endpoint + client_allowed_methods = _cinfo.get( + f"{endpoint.endpoint_name}_client_authn_method", _cinfo.get("client_authn_method") ) + if client_allowed_methods is not None and auth_info["method"] not in client_allowed_methods: + logger.info( + f"Allowed methods for client: {client_id} at endpoint: {endpoint.name} are: " + f"`{', '.join(client_allowed_methods)}`" + ) + auth_info = {} + continue + break # store what authn method was used - if auth_info.get("method"): + if "method" in auth_info and client_id: _request_type = request.__class__.__name__ _used_authn_method = _cinfo.get("auth_method") if _used_authn_method: diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index 24e7b3f6..ff5ce797 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -132,6 +132,9 @@ def set_client_authn_methods(self, **kwargs): kwargs[self.auth_method_attribute] = _methods elif _methods is not None: # [] or '' or something not None but regarded as nothing. self.client_authn_method = ["none"] # Ignore default value + elif self.default_capabilities: + self.client_authn_method = self.default_capabilities.get("client_authn_method") + self.endpoint_info = construct_provider_info(self.default_capabilities, **kwargs) return kwargs def get_provider_info_attributes(self): @@ -249,6 +252,8 @@ def client_authentication(self, request: Message, http_info: Optional[dict] = No 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/introspection.py b/src/idpyoidc/server/oauth2/introspection.py index f36d9503..11b29cca 100644 --- a/src/idpyoidc/server/oauth2/introspection.py +++ b/src/idpyoidc/server/oauth2/introspection.py @@ -6,6 +6,7 @@ from idpyoidc.server.endpoint import Endpoint from idpyoidc.server.token.exception import UnknownToken from idpyoidc.server.token.exception import WrongTokenClass +from idpyoidc.server.exception import ToOld LOGGER = logging.getLogger(__name__) @@ -103,7 +104,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs _session_info = _context.session_manager.get_session_info_by_token( request_token, grant=True ) - except (UnknownToken, WrongTokenClass): + except (UnknownToken, WrongTokenClass, ToOld): return {"response_args": _resp} grant = _session_info["grant"] diff --git a/src/idpyoidc/server/oauth2/token_helper.py b/src/idpyoidc/server/oauth2/token_helper.py index 0475abfc..3fb70702 100755 --- a/src/idpyoidc/server/oauth2/token_helper.py +++ b/src/idpyoidc/server/oauth2/token_helper.py @@ -75,7 +75,7 @@ def _mint_token( token_args = meth(_context, client_id, token_args) if token_args: - _args = {"token_args": token_args} + _args = token_args else: _args = {} @@ -258,7 +258,6 @@ def process_request(self, req: Union[Message, dict], **kwargs): if ( issue_refresh and "refresh_token" in _supports_minting - and "refresh_token" in grant_types_supported ): try: refresh_token = self._mint_token( @@ -370,7 +369,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): token_type = "DPoP" token = _grant.get_token(token_value) - scope = _grant.find_scope(token.based_on) + scope = _grant.find_scope(token) if "scope" in req: scope = req["scope"] access_token = self._mint_token( @@ -543,6 +542,27 @@ def post_parse_request(self, request, client_id="", **kwargs): ) 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"]) + + if not scopes: + logger.error("All requested scopes have been filtered out.") + return self.error_cls( + error="invalid_scope", error_description="Invalid requested scopes" + ) + + _requested_token_type = resp.get( + "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" + ) + _token_class = self.token_types_mapping[_requested_token_type] + if _token_class == "refresh_token" and "offline_access" not in scopes: + return TokenErrorResponse( + error="invalid_request", + error_description="Exchanging this subject token to refresh token forbidden", + ) return resp @@ -572,7 +592,7 @@ def _enforce_policy(self, request, token, config): error_description="Unsupported requested token type", ) - request_info = dict(scope=request.get("scope", [])) + request_info = dict(scope=request.get("scope", token.scope)) try: check_unknown_scopes_policy(request_info, request["client_id"], _context) except UnAuthorizedClientScope: @@ -602,11 +622,11 @@ def _enforce_policy(self, request, token, config): logger.error(f"Error while executing the {fn} policy callable: {e}") return self.error_cls(error="server_error", error_description="Internal server error") - def token_exchange_response(self, token): + def token_exchange_response(self, token, issued_token_type): response_args = {} response_args["access_token"] = token.value response_args["scope"] = token.scope - response_args["issued_token_type"] = token.token_class + response_args["issued_token_type"] = issued_token_type if token.expires_at: response_args["expires_in"] = token.expires_at - utc_time_sans_frac() @@ -636,6 +656,7 @@ def process_request(self, request, **kwargs): error="invalid_request", error_description="Subject token invalid" ) + grant=_session_info["grant"] token = _mngr.find_token(_session_info["branch_id"], request["subject_token"]) _requested_token_type = request.get( "requested_token_type", "urn:ietf:params:oauth:token-type:access_token" @@ -650,16 +671,19 @@ def process_request(self, request, **kwargs): if "dpop_signing_alg_values_supported" in _context.provider_info: if request.get("dpop_jkt"): _token_type = "DPoP" + scopes = request.get("scope", []) if request["client_id"] != _session_info["client_id"]: _token_usage_rules = _context.authz.usage_rules(request["client_id"]) sid = _mngr.create_exchange_session( exchange_request=request, + original_grant=grant, original_session_id=sid, user_id=_session_info["user_id"], client_id=request["client_id"], token_usage_rules=_token_usage_rules, + scopes=scopes, ) try: @@ -676,6 +700,10 @@ def process_request(self, request, **kwargs): else: resources = request.get("audience") + _token_args = None + if resources: + _token_args={"resources": resources} + try: new_token = self._mint_token( token_class=_token_class, @@ -683,10 +711,11 @@ def process_request(self, request, **kwargs): session_id=sid, client_id=request["client_id"], based_on=token, - scope=request.get("scope"), - token_args={"resources": resources}, + scope=scopes, + token_args=_token_args, token_type=_token_type, ) + new_token.expires_at = token.expires_at except MintingNotAllowed: logger.error(f"Minting not allowed for {_token_class}") return self.error_cls( @@ -694,7 +723,7 @@ def process_request(self, request, **kwargs): error_description="Token Exchange not allowed with that token", ) - return self.token_exchange_response(token=new_token) + return self.token_exchange_response(new_token, _requested_token_type) def _validate_configuration(self, config): if "requested_token_types_supported" not in config: @@ -759,18 +788,16 @@ def validate_token_exchange_policy(request, context, subject_token, **kwargs): if "offline_access" not in subject_token.scope: return TokenErrorResponse( error="invalid_request", - error_description=f"Exchange {request['subject_token_type']} to refresh token " - f"forbidden", + error_description=f"Exchange {request['subject_token_type']} to refresh token forbidden", ) - if "scope" in request: - scopes = list(set(request.get("scope")).intersection(kwargs.get("scope"))) - if scopes: - request["scope"] = scopes - else: - return TokenErrorResponse( - error="invalid_request", - error_description="No supported scope requested", - ) + scopes = request.get("scope", subject_token.scope) + scopes = list(set(scopes).intersection(subject_token.scope)) + if kwargs.get("scope"): + scopes = list(set(scopes).intersection(kwargs.get("scope"))) + if scopes: + request["scope"] = scopes + else: + request.pop("scope") return request diff --git a/src/idpyoidc/server/oidc/session.py b/src/idpyoidc/server/oidc/session.py index 5768cf5b..716b8277 100644 --- a/src/idpyoidc/server/oidc/session.py +++ b/src/idpyoidc/server/oidc/session.py @@ -361,7 +361,7 @@ def parse_request(self, request, http_info=None, **kwargs): # Verify that the client is allowed to do this auth_info = self.client_authentication(request, http_info, **kwargs) - if not auth_info or auth_info["method"] == "none": + if not auth_info: pass elif isinstance(auth_info, ResponseMessage): return auth_info diff --git a/src/idpyoidc/server/oidc/token_helper.py b/src/idpyoidc/server/oidc/token_helper.py index d319b9e2..34603036 100755 --- a/src/idpyoidc/server/oidc/token_helper.py +++ b/src/idpyoidc/server/oidc/token_helper.py @@ -122,7 +122,6 @@ def process_request(self, req: Union[Message, dict], **kwargs): if ( issue_refresh and "refresh_token" in _supports_minting - and "refresh_token" in grant_types_supported ): try: refresh_token = self._mint_token( @@ -237,7 +236,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): token_type = "DPoP" token = _grant.get_token(token_value) - scope = _grant.find_scope(token.based_on) + scope = _grant.find_scope(token) if "scope" in req: scope = req["scope"] access_token = self._mint_token( diff --git a/src/idpyoidc/server/oidc/userinfo.py b/src/idpyoidc/server/oidc/userinfo.py index 6b5473d0..ae6e87b5 100755 --- a/src/idpyoidc/server/oidc/userinfo.py +++ b/src/idpyoidc/server/oidc/userinfo.py @@ -182,7 +182,7 @@ def parse_request(self, request, http_info=None, **kwargs): try: auth_info = self.client_authentication(request, http_info, **kwargs) except ClientAuthenticationError as e: - return self.error_cls(error="invalid_token", error_description=e.args[0]) + return self.error_cls(error="invalid_token", error_description="Invalid token") if isinstance(auth_info, ResponseMessage): return auth_info diff --git a/src/idpyoidc/server/session/grant.py b/src/idpyoidc/server/session/grant.py index de54c4bc..47b490c7 100644 --- a/src/idpyoidc/server/session/grant.py +++ b/src/idpyoidc/server/session/grant.py @@ -184,6 +184,7 @@ def payload_arguments( endpoint_context, item: SessionToken, claims_release_point: str, + scope: Optional[dict] = None, extra_payload: Optional[dict] = None, secondary_identifier: str = "", ) -> dict: @@ -210,6 +211,10 @@ def payload_arguments( payload[_out] = _val payload["jti"] = uuid1().hex + + if scope is None: + scope = self.scope + payload["scope"] = scope if extra_payload: payload.update(extra_payload) @@ -359,6 +364,7 @@ def mint_token( endpoint_context, item=item, claims_release_point=claims_release_point, + scope=scope, extra_payload=handler_args, secondary_identifier=_secondary_identifier, ) @@ -474,7 +480,7 @@ def get_usage_rules(token_type, endpoint_context, grant, client_id): class ExchangeGrant(Grant): parameter = Grant.parameter.copy() - parameter.update({"users": []}) + parameter.update({"exchange_request": TokenExchangeRequest, "original_session_id": ""}) type = "exchange_grant" def __init__( @@ -483,6 +489,8 @@ def __init__( claims: Optional[dict] = None, resources: Optional[list] = None, authorization_details: Optional[dict] = None, + authorization_request: Optional[Message] = None, + authentication_event: Optional[AuthnEvent] = None, issued_token: Optional[list] = None, usage_rules: Optional[dict] = None, exchange_request: Optional[TokenExchangeRequest] = None, @@ -501,6 +509,8 @@ def __init__( claims=claims, resources=resources, authorization_details=authorization_details, + authorization_request=authorization_request, + authentication_event=authentication_event, issued_token=issued_token, usage_rules=usage_rules, issued_at=issued_at, @@ -517,3 +527,77 @@ def __init__( } self.exchange_request = exchange_request self.original_branch_id = original_branch_id + + def payload_arguments( + self, + session_id: str, + endpoint_context, + item: SessionToken, + claims_release_point: str, + scope: Optional[dict] = None, + extra_payload: Optional[dict] = None, + secondary_identifier: str = "", + ) -> dict: + """ + + :param session_id: Session ID + :param endpoint_context: EndPoint Context + :param claims_release_point: One of "userinfo", "introspection", "id_token", "access_token" + :param scope: scope from the request + :param extra_payload: + :param secondary_identifier: Used if the claims returned are also based on rules for + another release_point + :param item: A SessionToken instance + :type item: SessionToken + :return: dictionary containing information to place in a token value + """ + payload = {} + for _in, _out in [("scope", "scope"), ("resources", "aud")]: + _val = getattr(item, _in) + if _val: + payload[_out] = _val + else: + _val = getattr(self, _in) + if _val: + payload[_out] = _val + + payload["jti"] = uuid1().hex + + if scope is None: + scope = self.scope + + payload = {"scope": scope, "aud": self.resources, "jti": uuid1().hex} + + if extra_payload: + payload.update(extra_payload) + + _jkt = self.extra.get("dpop_jkt") + if _jkt: + payload["cnf"] = {"jkt": _jkt} + + if self.exchange_request: + client_id = self.exchange_request.get("client_id") + if client_id: + payload.update({"client_id": client_id, "sub": self.sub}) + + if item.claims: + _claims_restriction = item.claims + else: + _claims_restriction = endpoint_context.claims_interface.get_claims( + session_id, + scopes=scope, + claims_release_point=claims_release_point, + secondary_identifier=secondary_identifier, + ) + + user_id, _, _ = endpoint_context.session_manager.decrypt_session_id(session_id) + user_info = endpoint_context.claims_interface.get_user_claims(user_id, _claims_restriction) + payload.update(user_info) + + # Should I add the acr value + if self.add_acr_value(claims_release_point): + payload["acr"] = self.authentication_event["authn_info"] + elif self.add_acr_value(secondary_identifier): + payload["acr"] = self.authentication_event["authn_info"] + + return payload diff --git a/src/idpyoidc/server/session/manager.py b/src/idpyoidc/server/session/manager.py index 563e411a..0431719a 100644 --- a/src/idpyoidc/server/session/manager.py +++ b/src/idpyoidc/server/session/manager.py @@ -223,6 +223,7 @@ def create_grant( def create_exchange_grant( self, exchange_request: TokenExchangeRequest, + original_grant: Grant, original_session_id: str, user_id: str, client_id: Optional[str] = "", @@ -241,11 +242,13 @@ def create_exchange_grant( """ return self.add_exchange_grant( + authentication_event=original_grant.authentication_event, + authorization_request=original_grant.authorization_request, exchange_request=exchange_request, original_branch_id=original_session_id, path=self.make_path(user_id=user_id, client_id=client_id), + sub=original_grant.sub, token_usage_rules=token_usage_rules, - sub=self.sub_func[sub_type](user_id, salt=self.get_salt(), sector_identifier=""), scope=scopes ) @@ -286,6 +289,7 @@ def create_session( def create_exchange_session( self, exchange_request: TokenExchangeRequest, + original_grant: Grant, original_session_id: str, user_id: str, client_id: Optional[str] = "", @@ -309,6 +313,7 @@ def create_exchange_session( return self.create_exchange_grant( exchange_request=exchange_request, + original_grant=original_grant, original_session_id=original_session_id, user_id=user_id, client_id=client_id, diff --git a/tests/test_server_17_client_authn.py b/tests/test_server_17_client_authn.py index 4575ecd8..b26b49f3 100644 --- a/tests/test_server_17_client_authn.py +++ b/tests/test_server_17_client_authn.py @@ -337,7 +337,7 @@ def create_method(self): def test_bearer_body(self): request = {"access_token": "1234567890"} - assert self.method.verify(request) == {"token": "1234567890", "method": "bearer_body"} + assert self.method.verify(request, get_client_id_from_token=get_client_id_from_token) == {"token": "1234567890", "method": "bearer_body"} def test_bearer_body_no_token(self): request = {} @@ -504,13 +504,12 @@ def test_verify_per_client_per_endpoint(self): ) assert res == {"method": "public", "client_id": client_id} - with pytest.raises(ClientAuthenticationError) as e: - verify_client( - self.endpoint_context, - request, - endpoint=self.server.server_get("endpoint", "endpoint_1"), - ) - assert e.value.args[0] == "Failed to verify client" + res = verify_client( + self.endpoint_context, + request, + endpoint=self.server.server_get("endpoint", "endpoint_1"), + ) + assert res == {} request = {"client_id": client_id, "client_secret": client_secret} res = verify_client( diff --git a/tests/test_server_20d_client_authn.py b/tests/test_server_20d_client_authn.py index e81d26dd..a63ff1d8 100755 --- a/tests/test_server_20d_client_authn.py +++ b/tests/test_server_20d_client_authn.py @@ -292,7 +292,7 @@ def create_method(self): def test_bearer_body(self): request = {"access_token": "1234567890"} - assert self.method.verify(request) == {"token": "1234567890", "method": "bearer_body"} + assert self.method.verify(request, get_client_id_from_token=get_client_id_from_token) == {"token": "1234567890", "method": "bearer_body"} def test_bearer_body_no_token(self): request = {} @@ -457,14 +457,13 @@ def test_verify_per_client_per_endpoint(self): ) assert res == {"method": "public", "client_id": client_id} - with pytest.raises(ClientAuthenticationError) as e: - verify_client( - self.endpoint_context, - request, - endpoint=self.server.server_get("endpoint", "token"), - ) - assert e.value.args[0] == "Failed to verify client" - + res = verify_client( + self.endpoint_context, + request, + endpoint=self.server.server_get("endpoint", "token"), + ) + assert res == {} + request = {"client_id": client_id, "client_secret": client_secret} res = verify_client( self.endpoint_context, diff --git a/tests/test_server_23_oidc_registration_endpoint.py b/tests/test_server_23_oidc_registration_endpoint.py index 5b2ef4ae..64cb2a1b 100755 --- a/tests/test_server_23_oidc_registration_endpoint.py +++ b/tests/test_server_23_oidc_registration_endpoint.py @@ -127,7 +127,7 @@ def create_endpoint(self): "registration": { "path": "registration", "class": Registration, - "kwargs": {"client_auth_method": None}, + "kwargs": {"client_authn_method": ["none"]}, }, "authorization": { "path": "authorization", diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py index f532db02..04748917 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -457,9 +457,14 @@ def test_jwt_unknown_key(self): _resp = self.introspection_endpoint.process_request(_req) assert _resp["response_args"]["active"] is False - def test_expired_access_token(self): + def test_expired_access_token(self, monkeypatch): access_token = self._get_access_token(AUTH_REQ) - access_token.expires_at = utc_time_sans_frac() - 1000 + lifetime = self.session_manager.token_handler.handler["access_token"].lifetime + + def mock(): + return utc_time_sans_frac() + lifetime + 1 + + monkeypatch.setattr("idpyoidc.server.token.utc_time_sans_frac", mock) _context = self.introspection_endpoint.server_get("endpoint_context") diff --git a/tests/test_server_32_oidc_read_registration.py b/tests/test_server_32_oidc_read_registration.py index 1f7670ad..2e803ba7 100644 --- a/tests/test_server_32_oidc_read_registration.py +++ b/tests/test_server_32_oidc_read_registration.py @@ -95,7 +95,7 @@ def create_endpoint(self): "registration": { "path": "registration", "class": Registration, - "kwargs": {"client_auth_method": None}, + "kwargs": {"client_authn_method": ["none"]}, }, "registration_api": { "path": "registration_api", diff --git a/tests/test_server_36_oauth2_token_exchange.py b/tests/test_server_36_oauth2_token_exchange.py index 8b60d8b3..839d4e97 100644 --- a/tests/test_server_36_oauth2_token_exchange.py +++ b/tests/test_server_36_oauth2_token_exchange.py @@ -118,7 +118,10 @@ def create_endpoint(self): "introspection": { "path": "introspection", "class": "idpyoidc.server.oauth2.introspection.Introspection", - "kwargs": {}, + "kwargs": { + "client_authn_method": ["client_secret_post"], + "enable_claims_per_client": False, + }, }, }, "authentication": { @@ -182,6 +185,13 @@ def create_endpoint(self): "redirect_uris": [("https://example.com/cb", None)], "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", + "grant_types_supported": [ + "authorization_code", + "implicit", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "refresh_token", + "urn:ietf:params:oauth:grant-type:token-exchange" + ], "response_types": ["code", "token", "code id_token", "id_token"], "allowed_scopes": ["openid", "profile", "offline_access"], } @@ -346,7 +356,7 @@ def test_token_exchange_per_client(self, token): "policy": { "": { "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", - "kwargs": {"scope": ["openid"]}, + "kwargs": {"scope": ["openid", "offline_access"]}, } }, } @@ -386,6 +396,173 @@ def test_token_exchange_per_client(self, token): "issued_token_type", } + def test_token_exchange_scopes_per_client(self): + """ + Test that a client that requests offline_access in a Token Exchange request + only get it if the subject token has it in its scope set, if it is permitted + by the policy and if it is present in the clients allowed scopes. + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["openid", "profile", "offline_access"] + }, + } + }, + } + + self.endpoint_context.cdb["client_1"]["allowed_scopes"] = ["openid", "email", "profile", "offline_access"] + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:access_token", + scope="openid profile offline_access" + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + # Note that offline_access is filtered because subject_token has no offline_access + # in its scope + assert set(_resp["response_args"]["scope"]) == set(["profile", "openid"]) + + def test_token_exchange_unsupported_scopes_per_client(self): + """ + Test that unsupported clients are handled appropriatelly + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["openid", "profile", "offline_access"] + }, + } + }, + "allowed_scopes": ["openid", "email", "profile", "offline_access"] + } + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:access_token", + scope="email" + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert "scope" not in _resp + + def test_token_exchange_no_scopes_requested(self): + """ + Test that the correct scopes are returned when no scopes requested by the client + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["openid", "offline_access"] + }, + } + }, + "allowed_scopes": ["openid", "email", "profile", "offline_access"] + } + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:access_token" + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["response_args"]["scope"] == ["openid"] + def test_additional_parameters(self): """ Test that a token exchange with additional parameters including @@ -438,7 +615,12 @@ def test_token_exchange_fails_if_disabled(self): Test that token exchange fails if it's not included in Token's grant_types_supported (that are set in its helper attribute). """ - del self.endpoint.helper["urn:ietf:params:oauth:grant-type:token-exchange"] + self.endpoint_context.cdb["client_1"]["grant_types_supported"] = [ + 'authorization_code', + 'implicit', + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'refresh_token' + ] areq = AUTH_REQ.copy() @@ -840,3 +1022,490 @@ def test_invalid_token(self): assert set(_resp.keys()) == {"error", "error_description"} assert _resp["error"] == "invalid_request" assert _resp["error_description"] == "Subject token invalid" + + def test_token_exchange_unsupported_scope_requested_1(self): + """ + Configuration: + - grant_types_supported: [authorization_code, refresh_token, ...:token-exchange] + - allowed_scopes: [profile, offline_access] + - requested_token_type: "...:access_token" + Scenario: + Client1 has an access_token1 (with offline_access, openid and profile scope). + Then, client1 exchanges access_token1 for a new access_token1_13 with scope offline_access + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["offline_access", "profile"] + }, + } + }, + } + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + areq["scope"].append("offline_access") + + self.endpoint_context.cdb["client_1"]["allowed_scopes"] = ["offline_access", "profile"] + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:access_token", + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"offline_access", "profile"} + + token_exchange_req["scope"] = "profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"profile"} + + token_exchange_req["scope"] = "offline_access" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"offline_access"} + + token_exchange_req["scope"] = "offline_access profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"offline_access", "profile"} + + def test_token_exchange_unsupported_scope_requested_2(self): + """ + Configuration: + - grant_types_supported: [authorization_code, refresh_token, ...:token-exchange] + - allowed_scopes: [profile] + - requested_token_type: "...:access_token" + Scenario: + Client1 has an access_token1 (with openid and profile scope). + Then, client1 exchanges access_token1 for a new access_token1_13 with scope offline_access + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["profile"] + }, + } + }, + } + self.endpoint_context.cdb["client_1"]["allowed_scopes"] = ["openid", "profile"] + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + areq["scope"].append("offline_access") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:access_token", + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"profile"} + + token_exchange_req["scope"] = "profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["response_args"]["scope"] == ["profile"] + + token_exchange_req["scope"] = "offline_access" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_scope" + assert ( + _resp["error_description"] + == "Invalid requested scopes" + ) + + token_exchange_req["scope"] = "offline_access profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["response_args"]["scope"] == ["profile"] + + def test_token_exchange_unsupported_scope_requested_3(self): + """ + Configuration: + - grant_types_supported: [authorization_code, ...:token-exchange] + - allowed_scopes: [offline_access, profile] + - requested_token_type: "...:access_token" + Scenario: + Client1 has an access_token1 (with openid and profile scope). + Then, client1 exchanges access_token1 for a new access_token1_13 with scope offline_access + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["offline_access","profile"] + }, + } + }, + } + self.endpoint_context.cdb["client_1"]["grant_types_supported"] = [ + 'authorization_code', + 'implicit', + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'urn:ietf:params:oauth:grant-type:token-exchange' + ] + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + areq["scope"].append("offline_access") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:access_token", + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"profile", "offline_access"} + + token_exchange_req["scope"] = "profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["response_args"]["scope"] == ["profile"] + + token_exchange_req["scope"] = "offline_access" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["response_args"]["scope"] == ["offline_access"] + + _c_interface = self.introspection_endpoint.server_get("endpoint_context").claims_interface + grant.claims = { + "introspection": _c_interface.get_claims( + session_id, scopes=AUTH_REQ["scope"], claims_release_point="introspection" + ) + } + _req = self.introspection_endpoint.parse_request( + { + "token": _resp["response_args"]["access_token"], + "client_id": "client_1", + "client_secret": self.endpoint_context.cdb["client_1"]["client_secret"], + } + ) + _resp_intro = self.introspection_endpoint.process_request(_req) + assert _resp_intro["response_args"]["scope"] == "offline_access" + + token_exchange_req["scope"] = "offline_access profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"profile", "offline_access"} + + def test_token_exchange_unsupported_scope_requested_4(self): + """ + Configuration: + - grant_types_supported: [authorization_code, ...:token-exchange] + - allowed_scopes: [offline_access, profile] + - refresh_token removed from grant_types_supported + - requested_token_type: "...:access_token" + Scenario: + Client1 has an access_token1 (with openid and profile scope). + Then, client1 exchanges access_token1 for a new refresh token + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["offline_access", "profile"] + }, + } + }, + } + self.endpoint_context.cdb["client_1"]["grant_types_supported"] = [ + 'authorization_code', + 'implicit', + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'urn:ietf:params:oauth:grant-type:token-exchange' + ] + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + areq["scope"].append("offline_access") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:refresh_token", + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"profile", "offline_access"} + + token_exchange_req["scope"] = "profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( + _resp["error_description"] + == "Exchanging this subject token to refresh token forbidden" + ) + + token_exchange_req["scope"] = "offline_access" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"offline_access"} + + token_exchange_req["scope"] = "offline_access profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert set(_resp["response_args"]["scope"]) == {"profile", "offline_access"} + + token = grant.get_token(_resp["response_args"]["access_token"]) + assert token.token_class == "refresh_token" + + def test_token_exchange_unsupported_scope_requested_5(self): + """ + Configuration: + - grant_types_supported: [authorization_code, ...:token-exchange] + - allowed_scopes: [profile] + - requested_token_type: "...:access_token" + Scenario: + Client1 has an access_token1 (with openid and profile scope). + Then, client1 exchanges access_token1 for a new refresh token + """ + self.endpoint_context.cdb["client_1"]["token_exchange"] = { + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "requested_token_types_supported": [ + "urn:ietf:params:oauth:token-type:access_token", + "urn:ietf:params:oauth:token-type:refresh_token", + ], + "default_requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "policy": { + "": { + "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", + "kwargs": { + "scope": ["profile"] + }, + } + }, + } + + areq = AUTH_REQ.copy() + areq["scope"].append("profile") + areq["scope"].append("offline_access") + + session_id = self._create_session(areq) + grant = self.endpoint_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", + requested_token_type="urn:ietf:params:oauth:token-type:refresh_token", + ) + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( + _resp["error_description"] + == "Exchanging this subject token to refresh token forbidden" + ) + + token_exchange_req["scope"] = "profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( + _resp["error_description"] + == "Exchanging this subject token to refresh token forbidden" + ) + + token_exchange_req["scope"] = "offline_access" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_scope" + assert ( + _resp["error_description"] + == "Invalid requested scopes" + ) + + token_exchange_req["scope"] = "offline_access profile" + + _req = self.endpoint.parse_request( + token_exchange_req.to_urlencoded(), + {"headers": {"authorization": "Basic {}".format("Y2xpZW50XzE6aGVtbGlndA==")}}, + ) + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( + _resp["error_description"] + == "Exchanging this subject token to refresh token forbidden" + ) + diff --git a/tests/test_server_60_dpop.py b/tests/test_server_60_dpop.py index cd0301ef..7b74e172 100644 --- a/tests/test_server_60_dpop.py +++ b/tests/test_server_60_dpop.py @@ -164,7 +164,11 @@ def create_endpoint(self): "class": Authorization, "kwargs": {}, }, - "token": {"path": "{}/token", "class": Token, "kwargs": {}}, + "token": { + "path": "{}/token", + "class": Token, + "kwargs": {"client_authn_method": ["none"]}, + }, }, "client_authn": verify_client, "authentication": { diff --git a/tests/test_tandem_10_token_exchange.py b/tests/test_tandem_10_token_exchange.py index bf2c2649..b462b6c4 100644 --- a/tests/test_tandem_10_token_exchange.py +++ b/tests/test_tandem_10_token_exchange.py @@ -340,8 +340,8 @@ def test_token_exchange(self, token): "issued_token_type", } - assert _te_resp["issued_token_type"] == list(token.keys())[0] - assert _te_resp["scope"] == _scope + assert _te_resp["issued_token_type"] == token[list(token.keys())[0]] + assert set(_te_resp["scope"]) == set(_scope) @pytest.mark.parametrize( "token", @@ -367,7 +367,7 @@ def test_token_exchange_per_client(self, token): "": { "callable": "idpyoidc.server.oauth2.token_helper.validate_token_exchange_policy", - "kwargs": {"scope": ["openid"]}, + "kwargs": {"scope": ["openid", "offline_access"]}, } }, } @@ -395,8 +395,8 @@ def test_token_exchange_per_client(self, token): "issued_token_type", } - assert _te_resp["issued_token_type"] == list(token.keys())[0] - assert _te_resp["scope"] == _scope + assert _te_resp["issued_token_type"] == token[list(token.keys())[0]] + assert set(_te_resp["scope"]) == set(_scope) def test_additional_parameters(self): """