diff --git a/doc/intro.rst b/doc/intro.rst index a94082c0..19ff6f70 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -21,6 +21,7 @@ IdpyOIDC implements the following standards: * `OpenID Connect Front-Channel Logout 1.0 `_ * `OAuth2 Token introspection `_ * `OAuth2 Token exchange `_ +* `OAuth2 Resource Indicators `_ * `The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR) `_ It also comes with the following `add_on` modules. diff --git a/doc/server/contents/conf.rst b/doc/server/contents/conf.rst index 48bbaa35..d34503a3 100644 --- a/doc/server/contents/conf.rst +++ b/doc/server/contents/conf.rst @@ -884,3 +884,67 @@ idpyoidc\.server\.configure module :undoc-members: :show-inheritance: + +============== +Resource Indicators +============== +There are two possible ways to configure Resource Indicators in OIDC-OP, globally and per-client. +For the first case the configuration is passed in the Authorization or Access Token endpoint arguments throught the +`resource_indicators` dictionary. + +If present, the resource indicators configuration should contain a `policy` dictionary +that defines the behaviour of the specific endpoint. The policy +is mapped to a dictionary with the keys `callable` (mandatory), which must be a +python callable or a string that represents the path to a python callable, and +`kwargs` (optional), which must be a dict of key-value arguments that will be +passed to the callable. + +The resource indicators configuration may also contain a `resource_servers_per_client` +dictionary that defines a mapping between oidc-op registered clients with key the equivalent `client id` and resources to whom this client +is eligible to request access. + + "resource_indicators":{ + "policy": { + "callable": validate_authorization_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": { + "CLIENT_1": ["RESOURCE_1"], + "CLIENT_2": ["RESOURCE_1", "RESOURCE_2"] + }, + }, + }, + }, + } + +For the per-client configuration a similar configuration scheme should be present in the client's +metadata under the `resource_indicators` key with slight difference. The `policy` mapping should be set a value for a +key `authorization_code` or `access_token` in order to indicate the endpoint that this resource indicators policy is reffered to. +In addition, the `resource_servers_per_client` value is a list of the permitted resources. + +For example:: + + "resource_indicators":{ + "authorization_code": { + "policy": { + "callable": validate_authorization_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": ["RESOURCE_1"], + }, + }, + }, + }, + } + +The policy callable accepts a specific argument list and must return the altered token +request or raise an exception. + +For example:: + + def validate_resource_indicators_policy(request, context, **kwargs): + if some_condition in request: + return TokenErrorResponse( + error="invalid_request", error_description="Some error occured" + ) + + return request + diff --git a/requirements-dev.txt b/requirements-dev.txt index 36f34b11..5c518bd6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ pytest-isort>=1.3.0 pytest-localserver>=0.5.0 flake8 bandit +urllib3<1.27 \ No newline at end of file diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index be2efd89..10166742 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -14,6 +14,7 @@ from cryptojwt.utils import as_bytes from cryptojwt.utils import b64e +from idpyoidc.exception import ImproperlyConfigured from idpyoidc.exception import ParameterError from idpyoidc.exception import URIError from idpyoidc.message import Message @@ -39,6 +40,7 @@ from idpyoidc.time_util import utc_time_sans_frac from idpyoidc.util import rndstr from idpyoidc.util import split_uri +from idpyoidc.util import importer logger = logging.getLogger(__name__) @@ -277,6 +279,53 @@ def check_unknown_scopes_policy(request_info, client_id, endpoint_context): raise UnAuthorizedClientScope() +def validate_resource_indicators_policy(request, context, **kwargs): + if "resource" not in request: + return oauth2.AuthorizationErrorResponse( + error="invalid_target", + error_description="Missing resource parameter", + ) + + 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 @@ -304,6 +353,8 @@ class Authorization(Endpoint): def __init__(self, server_get, **kwargs): Endpoint.__init__(self, server_get, **kwargs) + + self.resource_indicators_config = kwargs.get("resource_indicators", None) self.post_parse_request.append(self._do_request_uri) self.post_parse_request.append(self._post_parse_request) self.allowed_request_algorithms = AllowedAlgorithms(ALG_PARAMS) @@ -461,8 +512,45 @@ def _post_parse_request(self, request, client_id, endpoint_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 + + if resource_indicators_config is not None: + if "policy" not in resource_indicators_config: + policy = {"policy": {"callable": validate_resource_indicators_policy}} + resource_indicators_config.update(policy) + request = self._enforce_resource_indicators_policy(request, resource_indicators_config) + return request + def _enforce_resource_indicators_policy(self, request, config): + _context = self.server_get("endpoint_context") + + policy = config["policy"] + callable = policy["callable"] + 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(callable, str): + try: + fn = importer(callable) + except Exception: + raise ImproperlyConfigured(f"Error importing {callable} policy callable") + else: + fn = callable + try: + return fn(request, context=_context, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} policy callable: {e}") + return self.error_cls(error="server_error", error_description="Internal server error") + def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs): _context = self.server_get("endpoint_context") auth_id = kwargs.get("auth_method_id") @@ -750,10 +838,17 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict _mngr = _context.session_manager _sinfo = _mngr.get_session_info(sid, grant=True) + scope = [] + resource_scopes = [] if request.get("scope"): - aresp["scope"] = _context.scopes_handler.filter_scopes( - request["scope"], _sinfo["client_id"] - ) + scope = request.get("scope") + if request.get("resource"): + resource_scopes = [_context.cdb[s]["scope"] for s in request.get("resource") if s in _context.cdb.keys() and _context.cdb[s].get("scope")] + resource_scopes = [item for sublist in resource_scopes for item in sublist] + + aresp["scope"] = _context.scopes_handler.filter_scopes( + list(set(scope+resource_scopes)), _sinfo["client_id"] + ) rtype = set(request["response_type"][:]) handled_response_type = [] diff --git a/src/idpyoidc/server/oauth2/token_helper.py b/src/idpyoidc/server/oauth2/token_helper.py index 1dca34a5..0475abfc 100755 --- a/src/idpyoidc/server/oauth2/token_helper.py +++ b/src/idpyoidc/server/oauth2/token_helper.py @@ -102,6 +102,54 @@ def _mint_token( return token +def validate_resource_indicators_policy(request, context, **kwargs): + if "resource" not in request: + return TokenErrorResponse( + error="invalid_target", + error_description="Missing resource parameter", + ) + + resource_servers_per_client = kwargs["resource_servers_per_client"] + client_id = request["client_id"] + + resource_servers_per_client = kwargs.get("resource_servers_per_client", None) + + if isinstance(resource_servers_per_client, dict) and client_id not in resource_servers_per_client: + return TokenErrorResponse( + 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 TokenErrorResponse( + 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 TokenErrorResponse( + 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 AccessTokenHelper(TokenEndpointHelper): def process_request(self, req: Union[Message, dict], **kwargs): @@ -132,6 +180,24 @@ def process_request(self, req: Union[Message, dict], **kwargs): logger.warning("Client using token it was not given") return self.error_cls(error="invalid_grant", error_description="Wrong client") + _cinfo = self.endpoint.server_get("endpoint_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) + + if resource_indicators_config is not None: + if "policy" not in resource_indicators_config: + policy = {"policy": {"callable": 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 + if "grant_types_supported" in _context.cdb[client_id]: grant_types_supported = _context.cdb[client_id].get("grant_types_supported") else: @@ -154,12 +220,25 @@ def process_request(self, req: Union[Message, dict], **kwargs): logger.debug("All checks OK") issue_refresh = kwargs.get("issue_refresh", False) + + if resource_indicators_config is not None: + scope = req["scope"] + else: + scope = grant.scope + _response = { "token_type": "Bearer", - "scope": grant.scope, + "scope": scope, } if "access_token" in _supports_minting: + + resources = req.get("resource", None) + if resources: + token_args = {"resources": resources} + else: + token_args = None + try: token = self._mint_token( token_class="access_token", @@ -167,6 +246,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): session_id=_session_info["branch_id"], client_id=_session_info["client_id"], based_on=_based_on, + token_args=token_args ) except MintingNotAllowed as err: logger.warning(err) @@ -200,6 +280,26 @@ def process_request(self, req: Union[Message, dict], **kwargs): return _response + def _enforce_resource_indicators_policy(self, request, config): + _context = self.endpoint.server_get("endpoint_context") + + policy = config["policy"] + callable = policy["callable"] + kwargs = policy.get("kwargs", {}) + + if isinstance(callable, str): + try: + fn = importer(callable) + except Exception: + raise ImproperlyConfigured(f"Error importing {callable} policy callable") + else: + fn = callable + try: + return fn(request, context=_context, **kwargs) + except Exception as e: + logger.error(f"Error while executing the {fn} policy callable: {e}") + return self.error_cls(error="server_error", error_description="Internal server error") + def post_parse_request( self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ): diff --git a/src/idpyoidc/server/scopes.py b/src/idpyoidc/server/scopes.py index 97bdf9c2..8c147b7c 100644 --- a/src/idpyoidc/server/scopes.py +++ b/src/idpyoidc/server/scopes.py @@ -1,3 +1,5 @@ +from idpyoidc.server.exception import ConfigurationError + # default set can be changed by configuration SCOPE2CLAIMS = { @@ -52,9 +54,7 @@ def __init__(self, server_get, allowed_scopes=None, scopes_to_claims=None): if not scopes_to_claims: scopes_to_claims = dict(SCOPE2CLAIMS) self._scopes_to_claims = scopes_to_claims - if not allowed_scopes: - allowed_scopes = list(scopes_to_claims.keys()) - self.allowed_scopes = allowed_scopes + self.allowed_scopes = list(scopes_to_claims.keys()) def get_allowed_scopes(self, client_id=None): """ @@ -67,11 +67,11 @@ def get_allowed_scopes(self, client_id=None): if client_id: client = self.server_get("endpoint_context").cdb.get(client_id) if client is not None: - if "allowed_scopes" in client: - allowed_scopes = client.get("allowed_scopes") - elif "scopes_to_claims" in client: - allowed_scopes = list(client.get("scopes_to_claims").keys()) - + try: + client_scopes = client["allowed_scopes"] + except: + raise ConfigurationError("No `allowed_scopes` are defined for client: %s" % client_id) + allowed_scopes = client_scopes return allowed_scopes def get_scopes_mapping(self, client_id=None): diff --git a/src/idpyoidc/server/session/.grant.py.swp b/src/idpyoidc/server/session/.grant.py.swp new file mode 100644 index 00000000..2272e35f Binary files /dev/null and b/src/idpyoidc/server/session/.grant.py.swp differ diff --git a/src/idpyoidc/server/session/manager.py b/src/idpyoidc/server/session/manager.py index 74803462..563e411a 100644 --- a/src/idpyoidc/server/session/manager.py +++ b/src/idpyoidc/server/session/manager.py @@ -192,6 +192,10 @@ def create_grant( sector_identifier = "" _claims = {} + resources = [] + if "resource" in auth_req: + resources = auth_req["resource"] + if self.node_type[0] == "user": kwargs = { "sub": self.sub_func[sub_type]( @@ -205,11 +209,15 @@ def create_grant( token_usage_rules=token_usage_rules, authorization_request=auth_req, authentication_event=authn_event, + sub=self.sub_func[sub_type]( + user_id, salt=self.get_salt(), sector_identifier=sector_identifier + ), + usage_rules=token_usage_rules, scope=scopes, claims=_claims, remember_token=self.remember_token, remove_inactive_token=self.remove_inactive_token, - **kwargs + resources=resources ) def create_exchange_grant( diff --git a/tests/test_server_01_claims.py b/tests/test_server_01_claims.py index 6ba28157..582b3975 100644 --- a/tests/test_server_01_claims.py +++ b/tests/test_server_01_claims.py @@ -139,6 +139,7 @@ def create_idtoken(self): "add_claims": { "always": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } self.endpoint_context.keyjar.add_symmetric("client_1", "hemligtochintekort", ["sig", "enc"]) self.claims_interface = self.endpoint_context.claims_interface diff --git a/tests/test_server_03_authz_handling.py b/tests/test_server_03_authz_handling.py index 3e0d347c..5dfc7d81 100644 --- a/tests/test_server_03_authz_handling.py +++ b/tests/test_server_03_authz_handling.py @@ -132,6 +132,7 @@ def create_idtoken(self): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } server.endpoint_context.keyjar.add_symmetric( "client_1", "hemligtochintekort", ["sig", "enc"] diff --git a/tests/test_server_08_id_token.py b/tests/test_server_08_id_token.py index 18494aa0..b1dac732 100644 --- a/tests/test_server_08_id_token.py +++ b/tests/test_server_08_id_token.py @@ -173,6 +173,7 @@ def create_session_manager(self): "always": {}, "by_scope": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } self.endpoint_context.keyjar.add_symmetric("client_1", "hemligtochintekort", ["sig", "enc"]) self.session_manager = self.endpoint_context.session_manager diff --git a/tests/test_server_10_session_manager.py b/tests/test_server_10_session_manager.py index 703a7aaa..381ee0bc 100644 --- a/tests/test_server_10_session_manager.py +++ b/tests/test_server_10_session_manager.py @@ -107,6 +107,7 @@ def create_session_manager(self): }, "refresh_token": {"supports_minting": ["id_token"]}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } } @@ -457,7 +458,9 @@ def test_token_usage_authz(self): self.server.get_endpoint_context, grant_config=grant_config ) - self.endpoint_context.cdb["client_1"] = {} + self.endpoint_context.cdb["client_1"] = { + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] + } token_usage_rules = self.endpoint_context.authz.usage_rules("client_1") @@ -511,7 +514,8 @@ def test_token_usage_client_config(self): "supports_minting": ["access_token", "refresh_token"], }, "refresh_token": {"supports_minting": ["access_token"]}, - } + }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } token_usage_rules = self.endpoint_context.authz.usage_rules("client_1") diff --git a/tests/test_server_20b_claims.py b/tests/test_server_20b_claims.py index 5372b566..d39ac8cd 100644 --- a/tests/test_server_20b_claims.py +++ b/tests/test_server_20b_claims.py @@ -125,6 +125,7 @@ def create_idtoken(self): "add_claims": { "always": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } server.endpoint_context.keyjar.add_symmetric( "client_1", "hemligtochintekort", ["sig", "enc"] diff --git a/tests/test_server_20c_authz_handling.py b/tests/test_server_20c_authz_handling.py index e7aab37b..8a7c1aa1 100644 --- a/tests/test_server_20c_authz_handling.py +++ b/tests/test_server_20c_authz_handling.py @@ -108,6 +108,7 @@ def create_idtoken(self): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } server.endpoint_context.keyjar.add_symmetric( "client_1", "hemligtochintekort", ["sig", "enc"] diff --git a/tests/test_server_20e_jwt_token.py b/tests/test_server_20e_jwt_token.py index 9744f8bd..9bd86599 100644 --- a/tests/test_server_20e_jwt_token.py +++ b/tests/test_server_20e_jwt_token.py @@ -207,6 +207,7 @@ def create_endpoint(self): "always": {}, "by_scope": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } self.session_manager = self.endpoint_context.session_manager self.user_id = "diana" @@ -410,6 +411,7 @@ def create_endpoint(self): "always": {}, "by_scope": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access", "webid"] } self.session_manager = self.endpoint_context.session_manager self.user_id = "diana" diff --git a/tests/test_server_20f_userinfo.py b/tests/test_server_20f_userinfo.py index f3c15ff6..70272ef9 100644 --- a/tests/test_server_20f_userinfo.py +++ b/tests/test_server_20f_userinfo.py @@ -199,6 +199,7 @@ def create_endpoint_context(self): "always": {}, "by_scope": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } self.session_manager = self.endpoint_context.session_manager self.claims_interface = ClaimsInterface(server.server_get) @@ -422,7 +423,9 @@ def conf(self): def create_endpoint_context(self, conf): self.server = Server(conf) self.endpoint_context = self.server.endpoint_context - self.endpoint_context.cdb["client1"] = {} + self.endpoint_context.cdb["client1"] = { + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access", "research_and_scholarship"] + } self.session_manager = self.endpoint_context.session_manager self.claims_interface = ClaimsInterface(self.server.server_get) self.user_id = "diana" @@ -477,19 +480,7 @@ def test_collect_user_info_scope_mapping_per_client(self, conf): self.session_manager = endpoint_context.session_manager claims_interface = endpoint_context.claims_interface endpoint_context.cdb["client1"] = { - "scopes_to_claims": { - "openid": ["sub"], - "research_and_scholarship": [ - "name", - "given_name", - "family_name", - "email", - "email_verified", - "sub", - "iss", - "eduperson_scoped_affiliation", - ], - } + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } _req = OIDR.copy() @@ -503,31 +494,6 @@ def test_collect_user_info_scope_mapping_per_client(self, conf): ) res = claims_interface.get_user_claims("diana", _restriction) - - assert res == { - "eduperson_scoped_affiliation": ["staff@example.org"], - "email": "diana@example.org", - "email_verified": False, - "family_name": "Krall", - "given_name": "Diana", - "name": "Diana Krall", - } - - def test_collect_user_info_allowed_scopes_per_client(self): - self.endpoint_context.cdb["client1"] = {"allowed_scopes": {"openid"}} - - _req = OIDR.copy() - _req["scope"] = "openid research_and_scholarship" - del _req["claims"] - - session_id = self._create_session(_req) - - _restriction = self.claims_interface.get_claims( - session_id=session_id, scopes=_req["scope"], claims_release_point="userinfo" - ) - - res = self.claims_interface.get_user_claims("diana", _restriction) - assert res == { "eduperson_scoped_affiliation": ["staff@example.org"], "email": "diana@example.org", diff --git a/tests/test_server_24_oauth2_authorization_endpoint.py b/tests/test_server_24_oauth2_authorization_endpoint.py index 01386f86..3f8e2d56 100755 --- a/tests/test_server_24_oauth2_authorization_endpoint.py +++ b/tests/test_server_24_oauth2_authorization_endpoint.py @@ -131,6 +131,13 @@ def get_cookie_value(cookie=None, name=None): 'response_types': - 'code' - 'token' + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client2: client_secret: "spraket" redirect_uris: @@ -138,6 +145,13 @@ def get_cookie_value(cookie=None, name=None): - ['https://app2.example.net/bar', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' """ @@ -479,6 +493,7 @@ def test_create_authn_response(self): "client_id": "client_id", "redirect_uris": [("https://rp.example.com/cb", {})], "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } session_id = self._create_session(request) @@ -557,6 +572,7 @@ def test_setup_auth_invalid_scope(self): "client_id": "client_id", "redirect_uris": [("https://rp.example.com/cb", {})], "id_token_signed_response_alg": "RS256", + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } _context = self.endpoint.server_get("endpoint_context") diff --git a/tests/test_server_24_oauth2_authorization_endpoint_jar.py b/tests/test_server_24_oauth2_authorization_endpoint_jar.py index 56953a2b..6a719758 100755 --- a/tests/test_server_24_oauth2_authorization_endpoint_jar.py +++ b/tests/test_server_24_oauth2_authorization_endpoint_jar.py @@ -108,6 +108,13 @@ def get_cookie_value(cookie=None, name=None): 'response_types': - 'code' - 'token' + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client2: client_secret: "spraket" redirect_uris: @@ -115,6 +122,13 @@ def get_cookie_value(cookie=None, name=None): - ['https://app2.example.net/bar', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' """ diff --git a/tests/test_server_24_oauth2_resource_indicators.py b/tests/test_server_24_oauth2_resource_indicators.py new file mode 100644 index 00000000..3993e2d8 --- /dev/null +++ b/tests/test_server_24_oauth2_resource_indicators.py @@ -0,0 +1,680 @@ +import io +import json +import os +from http.cookies import SimpleCookie +from urllib.parse import parse_qs +from urllib.parse import urlparse + +import pytest +import yaml +from cryptojwt.jwt import JWT +from cryptojwt.key_jar import init_key_jar +from cryptojwt import KeyJar +from cryptojwt.jwt import utc_time_sans_frac +from cryptojwt.utils import as_bytes +from cryptojwt.utils import b64e + +from idpyoidc.exception import ParameterError +from idpyoidc.exception import URIError +from idpyoidc.message.oauth2 import AuthorizationErrorResponse, TokenErrorResponse +from idpyoidc.message.oidc import AccessTokenRequest +from idpyoidc.message.oauth2 import AuthorizationRequest +from idpyoidc.message.oauth2 import AuthorizationResponse +from idpyoidc.server import Server +from idpyoidc.server.authn_event import create_authn_event +from idpyoidc.server.authz import AuthzHandling +from idpyoidc.server.configure import ASConfiguration +from idpyoidc.server.cookie_handler import CookieHandler +from idpyoidc.server.exception import InvalidRequest +from idpyoidc.server.exception import NoSuchAuthentication +from idpyoidc.server.exception import RedirectURIError +from idpyoidc.server.exception import ToOld +from idpyoidc.server.exception import UnAuthorizedClientScope +from idpyoidc.server.exception import UnknownClient +from idpyoidc.server.oauth2.authorization import FORM_POST +from idpyoidc.server.oauth2.authorization import Authorization +from idpyoidc.server.oauth2.token import Token +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 verify_uri +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 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 +from tests import SESSION_PARAMS + +KEYDEFS = [ + {"type": "RSA", "key": "", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]} +] + +COOKIE_KEYDEFS = [ + {"type": "oct", "kid": "sig", "use": ["sig"]}, + {"type": "oct", "kid": "enc", "use": ["enc"]}, +] + +RESPONSE_TYPES_SUPPORTED = [["code"], ["token"], ["code", "token"], ["none"]] + +CAPABILITIES = { + "grant_types_supported": [ + "authorization_code", + "implicit", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "refresh_token", + ] +} + +AUTH_REQ = AuthorizationRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + scope=["openid", "email", "profile"], + state="STATE", + response_type="code", + resource=["client_2"], +) + +AUTH_REQ_DICT = AUTH_REQ.to_dict() + +TOKEN_REQ = AccessTokenRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + state="STATE", + grant_type="authorization_code", + client_secret="hemligt", + resource=["client_3"], +) + +TOKEN_REQ_DICT = TOKEN_REQ.to_dict() + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +USERINFO_db = json.loads(open(full_path("users.json")).read()) + + +class SimpleCookieDealer(object): + def __init__(self, name=""): + self.name = name + + def create_cookie(self, value, typ, **kwargs): + cookie = SimpleCookie() + timestamp = str(utc_time_sans_frac()) + + _payload = "::".join([value, timestamp, typ]) + + bytes_load = _payload.encode("utf-8") + bytes_timestamp = timestamp.encode("utf-8") + + cookie_payload = [bytes_load, bytes_timestamp] + cookie[self.name] = (b"|".join(cookie_payload)).decode("utf-8") + try: + ttl = kwargs["ttl"] + except KeyError: + pass + else: + cookie[self.name]["expires"] = in_a_while(seconds=ttl) + + return cookie + + @staticmethod + def get_cookie_value(cookie=None, name=None): + if cookie is None or name is None: + return None + else: + try: + info, timestamp = cookie[name].split("|") + except (TypeError, AssertionError): + return None + else: + value = info.split("::") + if timestamp == value[1]: + return value + return None + + +client_yaml = """ +clients: + client_1: + "client_secret": 'hemligt' + "redirect_uris": + - ['https://example.com/cb', ''] + "client_salt": "salted" + 'token_endpoint_auth_method': 'client_secret_post' + 'response_types': + - 'code' + - 'token' + 'scope': + - 'test' + 'allowed_scopes': + - 'openid' + - 'profile' + client_2: + client_secret: "spraket" + redirect_uris: + - ['https://app1.example.net/foo', ''] + - ['https://app2.example.net/bar', ''] + response_types: + - code + 'allowed_scopes': + - 'openid' + - 'email' +""" + +RESOURCE_INDICATORS_DISABLED = { + "issuer": "https://example.com/", + "password": "mycket hemligt zebra", + "verify_ssl": False, + "capabilities": CAPABILITIES, + "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, + "token_handler_args": { + "jwks_def": { + "private_path": "private/token_jwks.json", + "read_only": False, + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], + }, + "code": {"lifetime": 600, "kwargs": {"crypt_conf": CRYPT_CONFIG}}, + "token": { + "class": "idpyoidc.server.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "add_claims_by_scope": True, + "aud": ["https://example.org/appl"], + }, + }, + "refresh": { + "class": "idpyoidc.server.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "aud": ["https://example.org/appl"], + }, + }, + "id_token": { + "class": "idpyoidc.server.token.id_token.IDToken", + "kwargs": { + "base_claims": { + "email": {"essential": True}, + "email_verified": {"essential": True}, + } + }, + }, + }, + "endpoint": { + "authorization": { + "path": "{}/authorization", + "class": Authorization, + "kwargs": { + "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, + }, + }, + "token": { + "path": "token", + "class": Token, + "kwargs": { + "client_authn_method": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + ], + }, + }, + }, + "authentication": { + "anon": { + "acr": "http://www.swamid.se/policy/assurance/al1", + "class": "idpyoidc.server.user_authn.user.NoAuthn", + "kwargs": {"user": "diana"}, + } + }, + "userinfo": {"class": UserInfo, "kwargs": {"db": USERINFO_db}}, + "template_dir": "template", + "cookie_handler": { + "class": CookieHandler, + "kwargs": { + "keys": {"key_defs": COOKIE_KEYDEFS}, + "name": { + "session": "oidc_op", + "register": "oidc_op_reg", + "session_management": "oidc_op_sman", + }, + }, + }, + "authz": { + "class": AuthzHandling, + "kwargs": { + "grant_config": { + "usage_rules": { + "authorization_code": { + "supports_minting": [ + "access_token", + "refresh_token", + "id_token", + ], + "max_usage": 1, + }, + "access_token": {}, + "refresh_token": { + "supports_minting": [ + "access_token", + "refresh_token", + "id_token", + ], + }, + }, + "expires_in": 43200, + } + }, + }, + "session_params": SESSION_PARAMS, +} + +RESOURCE_INDICATORS_ENABLED = { + "issuer": "https://example.com/", + "password": "mycket hemligt zebra", + "verify_ssl": False, + "capabilities": CAPABILITIES, + "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, + "token_handler_args": { + "jwks_def": { + "private_path": "private/token_jwks.json", + "read_only": False, + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], + }, + "code": {"lifetime": 600, "kwargs": {"crypt_conf": CRYPT_CONFIG}}, + "token": { + "class": "idpyoidc.server.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "add_claims_by_scope": True, + "aud": ["https://example.org/appl"], + }, + }, + "refresh": { + "class": "idpyoidc.server.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "aud": ["https://example.org/appl"], + }, + }, + "id_token": { + "class": "idpyoidc.server.token.id_token.IDToken", + "kwargs": { + "base_claims": { + "email": {"essential": True}, + "email_verified": {"essential": True}, + } + }, + }, + }, + "endpoint": { + "authorization": { + "path": "{}/authorization", + "class": Authorization, + "kwargs": { + "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": { + "callable": validate_authorization_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": { + "client_1": ["client_1", "client_2"], + }, + }, + } + }, + }, + }, + "token": { + "path": "token", + "class": Token, + "kwargs": { + "client_authn_method": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + ], + "resource_indicators": { + "policy": { + "callable": validate_token_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client": { + "client_1": ["client_2", "client_3"] + }, + }, + } + }, + }, + }, + }, + "authentication": { + "anon": { + "acr": "http://www.swamid.se/policy/assurance/al1", + "class": "idpyoidc.server.user_authn.user.NoAuthn", + "kwargs": {"user": "diana"}, + } + }, + "userinfo": {"class": UserInfo, "kwargs": {"db": USERINFO_db}}, + "template_dir": "template", + "cookie_handler": { + "class": CookieHandler, + "kwargs": { + "keys": {"key_defs": COOKIE_KEYDEFS}, + "name": { + "session": "oidc_op", + "register": "oidc_op_reg", + "session_management": "oidc_op_sman", + }, + }, + }, + "authz": { + "class": AuthzHandling, + "kwargs": { + "grant_config": { + "usage_rules": { + "authorization_code": { + "supports_minting": [ + "access_token", + "refresh_token", + "id_token", + ], + "max_usage": 1, + }, + "access_token": {}, + "refresh_token": { + "supports_minting": [ + "access_token", + "refresh_token", + "id_token", + ], + }, + }, + "expires_in": 43200, + } + }, + }, + "session_params": SESSION_PARAMS, +} + +class TestEndpoint(object): + @pytest.fixture(autouse=False) + def create_endpoint_ri_disabled(self): + conf = RESOURCE_INDICATORS_DISABLED + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + + endpoint_context = server.endpoint_context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + endpoint_context.cdb = _clients["clients"] + endpoint_context.keyjar.import_jwks( + endpoint_context.keyjar.export_jwks(True, ""), conf["issuer"] + ) + self.endpoint_context = endpoint_context + self.endpoint = server.server_get("endpoint", "authorization") + self.token_endpoint = server.server_get("endpoint", "token") + self.session_manager = endpoint_context.session_manager + self.user_id = "diana" + + self.rp_keyjar = KeyJar() + self.rp_keyjar.add_symmetric("client_1", "hemligtkodord1234567890") + self.endpoint.server_get("endpoint_context").keyjar.add_symmetric( + "client_1", "hemligtkodord1234567890" + ) + + @pytest.fixture(autouse=False) + def create_endpoint_ri_enabled(self): + conf = RESOURCE_INDICATORS_ENABLED + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + + endpoint_context = server.endpoint_context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + endpoint_context.cdb = _clients["clients"] + endpoint_context.keyjar.import_jwks( + endpoint_context.keyjar.export_jwks(True, ""), conf["issuer"] + ) + self.endpoint_context = endpoint_context + self.endpoint = server.server_get("endpoint", "authorization") + self.token_endpoint = server.server_get("endpoint", "token") + self.session_manager = endpoint_context.session_manager + self.user_id = "diana" + + self.rp_keyjar = KeyJar() + self.rp_keyjar.add_symmetric("client_1", "hemligtkodord1234567890") + self.endpoint.server_get("endpoint_context").keyjar.add_symmetric( + "client_1", "hemligtkodord1234567890" + ) + + def _create_session(self, auth_req, sub_type="public", sector_identifier=""): + if sector_identifier: + areq = auth_req.copy() + areq["sector_identifier_uri"] = sector_identifier + else: + areq = auth_req + + client_id = areq["client_id"] + ae = create_authn_event(self.user_id) + return self.session_manager.create_session( + ae, areq, self.user_id, client_id=client_id, sub_type=sub_type + ) + + def _mint_code(self, grant, client_id): + session_id = self.session_manager.encrypted_session_id(self.user_id, client_id, grant.id) + usage_rules = grant.usage_rules.get("authorization_code", {}) + _exp_in = usage_rules.get("expires_in") + + # Constructing an authorization code is now done + _code = grant.mint_token( + session_id=session_id, + endpoint_context=self.endpoint_context, + token_class="authorization_code", + token_handler=self.session_manager.token_handler["authorization_code"], + usage_rules=usage_rules, + resources=grant.resources + ) + + if _exp_in: + if isinstance(_exp_in, str): + _exp_in = int(_exp_in) + if _exp_in: + _code.expires_at = utc_time_sans_frac() + _exp_in + return _code + + def test_init(self, create_endpoint_ri_enabled): + assert self.endpoint + + def test_parse(self, create_endpoint_ri_enabled): + _req = self.endpoint.parse_request(AUTH_REQ_DICT) + assert isinstance(_req, AuthorizationRequest) + assert set(_req.keys()) == set(AUTH_REQ.keys()) + + def test_authorization_code_req_no_resource(self, create_endpoint_ri_enabled): + """ + Test that appropriate error message is returned when resource indicators is enabled + for the authorization endpoint and resource parameter is missing from request. + """ + endpoint_context = self.endpoint.server_get("endpoint_context") + msg = self.endpoint._post_parse_request({}, "client_1", endpoint_context) + assert "error" in msg + + request = AuthorizationRequest( + client_id="client_1", + response_type=["code"], + state="state", + nonce="nonce", + scope="openid", + ) + + msg = self.endpoint._post_parse_request(request, "client_1", endpoint_context) + assert "error" in msg + assert msg["error_description"] == "Missing resource parameter" + + def test_authorization_code_req_no_resource_indicators_disabled(self, create_endpoint_ri_disabled): + """ + Test successful authorization request when resource indicators is disabled. + """ + endpoint_context = self.endpoint.server_get("endpoint_context") + request = AUTH_REQ.copy() + del request["resource"] + + msg = self.endpoint._post_parse_request(request, "client_1", endpoint_context) + assert "error" not in msg + + def test_authorization_code_req(self, create_endpoint_ri_enabled): + """ + Test successful authorization request when resource indicators is enabled. + """ + endpoint_context = self.endpoint.server_get("endpoint_context") + request = AUTH_REQ.copy() + + msg = self.endpoint._post_parse_request(request, "client_1", endpoint_context) + assert "error" not in msg + + 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. + """ + endpoint_context = self.endpoint.server_get("endpoint_context") + endpoint_context.cdb["client_1"]["resource_indicators"] = { + "authorization_code": { + "policy": { + "callable": validate_authorization_resource_indicators_policy, + "kwargs": { + "resource_servers_per_client":["client_3"] + }, + }, + }, + } + request = AUTH_REQ.copy() + client_id = request["client_id"] + + msg = self.endpoint._post_parse_request(request, "client_1", endpoint_context) + assert "error" in msg + assert msg["error_description"] == f"Invalid resource requested by client {client_id}" + + def test_authorization_code_req_no_resource_client(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. + """ + request = AUTH_REQ.copy() + client_id = request["client_id"] + endpoint_context = self.endpoint.server_get("endpoint_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" + + def test_authorization_code_req_invalid_resource_client(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. + """ + request = AUTH_REQ.copy() + request["resource"] = "client_2" + client_id = request["client_id"] + endpoint_context = self.endpoint.server_get("endpoint_context") + + msg = self.endpoint._post_parse_request(request, client_id, endpoint_context) + + assert "error" in msg + assert msg["error"] == "invalid_target" + assert msg["error_description"] == f"Invalid resource requested by client {client_id}" + + def test_access_token_req(self, create_endpoint_ri_enabled): + """ + Test successful access_token request when resource indicators is enabled. + """ + self.endpoint.server_get("endpoint_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 + _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 set(access_token["aud"]) == set(["client_3", "client_1"]) + + def test_access_token_req_invalid_resource_client(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. + """ + 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_2" + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + + assert "error" in _resp + assert _resp["error"] == "invalid_target" + assert _resp["error_description"] == f"Invalid resource requested by client {client_id}" + + def test_create_authn_response(self, create_endpoint_ri_enabled): + """ + Test that the requested access_token has the correct scopes based on the allowed scopes of + the requested resources + """ + self.endpoint.server_get("endpoint_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["scope"] = ["openid", "profile"] + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + assert "response_args" in _resp + assert set(_resp["response_args"]["scope"]) == set(["openid", "profile"]) diff --git a/tests/test_server_24_oauth2_token_endpoint.py b/tests/test_server_24_oauth2_token_endpoint.py index 292f9b80..25c479f4 100644 --- a/tests/test_server_24_oauth2_token_endpoint.py +++ b/tests/test_server_24_oauth2_token_endpoint.py @@ -172,6 +172,7 @@ def create_endpoint(self, conf): "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"] } endpoint_context.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") self.session_manager = endpoint_context.session_manager @@ -445,6 +446,7 @@ def test_new_refresh_token(self, conf): "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"] } areq = AUTH_REQ.copy() @@ -484,6 +486,7 @@ def test_revoke_on_issue_refresh_token(self, conf): "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"] } self.token_endpoint.revoke_refresh_on_issue = True @@ -521,6 +524,7 @@ def test_revoke_on_issue_refresh_token_per_client(self, conf): "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"] } self.endpoint_context.cdb[AUTH_REQ["client_id"]]["revoke_refresh_on_issue"] = True areq = AUTH_REQ.copy() diff --git a/tests/test_server_24_oidc_authorization_endpoint.py b/tests/test_server_24_oidc_authorization_endpoint.py index 2e7e20ef..019349b0 100755 --- a/tests/test_server_24_oidc_authorization_endpoint.py +++ b/tests/test_server_24_oidc_authorization_endpoint.py @@ -126,6 +126,13 @@ def full_path(local_file): - 'code id_token' - 'id_token' - 'code id_token token' + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client2: client_secret: "spraket_sr.se" redirect_uris: @@ -140,6 +147,13 @@ def full_path(local_file): post_logout_redirect_uri: ['https://openidconnect.net/', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' """ @@ -592,6 +606,7 @@ def test_create_authn_response_id_token(self): "client_id": "client_id", "redirect_uris": [("https://rp.example.com/cb", {})], "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } session_id = self._create_session(request) @@ -619,6 +634,7 @@ def test_create_authn_response_id_token_request_claims(self): "client_id": "client_id", "redirect_uris": [("https://rp.example.com/cb", {})], "id_token_signed_response_alg": "ES256", + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } session_id = self._create_session(request) diff --git a/tests/test_server_26_oidc_userinfo_endpoint.py b/tests/test_server_26_oidc_userinfo_endpoint.py index 2d664b00..6bfa071e 100755 --- a/tests/test_server_26_oidc_userinfo_endpoint.py +++ b/tests/test_server_26_oidc_userinfo_endpoint.py @@ -192,6 +192,7 @@ def create_endpoint(self): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access", "research_and_scholarship"] } self.endpoint = self.server.server_get("endpoint", "userinfo") self.session_manager = self.endpoint_context.session_manager @@ -416,8 +417,6 @@ def test_scopes_to_claims_per_client(self): } def test_allowed_scopes(self): - self.endpoint_context.scopes_handler.allowed_scopes = list(SCOPE2CLAIMS.keys()) - _auth_req = AUTH_REQ.copy() _auth_req["scope"] = ["openid", "research_and_scholarship"] @@ -437,44 +436,16 @@ def test_allowed_scopes(self): _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req, http_info=http_info) - assert set(args["response_args"].keys()) == {"sub"} - - def test_allowed_scopes_per_client(self): - self.endpoint_context.cdb["client_1"]["scopes_to_claims"] = { - **SCOPE2CLAIMS, - "research_and_scholarship_2": [ - "name", - "given_name", - "family_name", - "email", - "email_verified", - "sub", - "eduperson_scoped_affiliation", - ], - } - self.endpoint_context.cdb["client_1"]["allowed_scopes"] = list(SCOPE2CLAIMS.keys()) - - _auth_req = AUTH_REQ.copy() - _auth_req["scope"] = ["openid", "research_and_scholarship_2"] - - session_id = self._create_session(_auth_req) - grant = self.session_manager[session_id] - access_token = self._mint_token("access_token", grant, session_id) - - self.endpoint.kwargs["add_claims_by_scope"] = True - self.endpoint.server_get("endpoint_context").claims_interface.add_claims_by_scope = True - grant.claims = { - "userinfo": self.endpoint.server_get("endpoint_context").claims_interface.get_claims( - session_id=session_id, scopes=_auth_req["scope"], claims_release_point="userinfo" - ) + assert set(args["response_args"].keys()) == { + "eduperson_scoped_affiliation", + "given_name", + "email_verified", + "email", + "family_name", + "name", + "sub" } - http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} - _req = self.endpoint.parse_request({}, http_info=http_info) - args = self.endpoint.process_request(_req, http_info=http_info) - - assert set(args["response_args"].keys()) == {"sub"} - def test_wrong_type_of_token(self): _auth_req = AUTH_REQ.copy() _auth_req["scope"] = ["openid", "research_and_scholarship"] diff --git a/tests/test_server_30_oidc_end_session.py b/tests/test_server_30_oidc_end_session.py index 317934e5..7b12ab32 100644 --- a/tests/test_server_30_oidc_end_session.py +++ b/tests/test_server_30_oidc_end_session.py @@ -209,6 +209,7 @@ def create_endpoint(self): "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], "post_logout_redirect_uri": [f"{CLI1}logout_cb", ""], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] }, "client_2": { "client_secret": "hemligare", @@ -217,6 +218,7 @@ def create_endpoint(self): "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], "post_logout_redirect_uri": [f"{CLI2}logout_cb", ""], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] }, } self.endpoint_context = endpoint_context diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py index 93f65e2c..f532db02 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -204,6 +204,7 @@ def create_endpoint(self, jwt_token): }, "by_scope": {}, }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } endpoint_context.keyjar.import_jwks_as_json( endpoint_context.keyjar.export_jwks_as_json(private=True), diff --git a/tests/test_server_33_oauth2_pkce.py b/tests/test_server_33_oauth2_pkce.py index ff44eb26..fbfc961f 100644 --- a/tests/test_server_33_oauth2_pkce.py +++ b/tests/test_server_33_oauth2_pkce.py @@ -97,6 +97,13 @@ def full_path(local_file): - 'code id_token' - 'id_token' - 'code id_token token' + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client2: client_secret: "spraket" redirect_uris: @@ -104,6 +111,13 @@ def full_path(local_file): - ['https://app2.example.net/bar', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client3: client_secret: '2222222222222222222222222222222222222222' redirect_uris: @@ -111,6 +125,13 @@ def full_path(local_file): post_logout_redirect_uri: ['https://openidconnect.net/', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' """ diff --git a/tests/test_server_34_oidc_sso.py b/tests/test_server_34_oidc_sso.py index 96846c88..99b2db41 100755 --- a/tests/test_server_34_oidc_sso.py +++ b/tests/test_server_34_oidc_sso.py @@ -103,6 +103,13 @@ def full_path(local_file): - 'code id_token' - 'id_token' - 'code id_token token' + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client_2: client_secret: "spraket_sr.se" client_id: client_2, @@ -111,6 +118,13 @@ def full_path(local_file): - ['https://app2.example.net/bar', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' client_3: client_id: client_3, client_secret: '2222222222222222222222222222222222222222' @@ -119,6 +133,13 @@ def full_path(local_file): post_logout_redirect_uri: ['https://openidconnect.net/', ''] response_types: - code + allowed_scopes: + - 'openid' + - 'profile' + - 'email' + - 'address' + - 'phone' + - 'offline_access' """ diff --git a/tests/test_server_35_oidc_token_endpoint.py b/tests/test_server_35_oidc_token_endpoint.py index 2666a54c..2faa76d6 100755 --- a/tests/test_server_35_oidc_token_endpoint.py +++ b/tests/test_server_35_oidc_token_endpoint.py @@ -211,6 +211,7 @@ def create_endpoint(self, conf): "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"] } endpoint_context.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") endpoint_context.userinfo = USERINFO @@ -774,6 +775,7 @@ def test_new_refresh_token(self, conf): "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"] } areq = AUTH_REQ.copy() @@ -813,6 +815,7 @@ def test_revoke_on_issue_refresh_token(self, conf): "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"] } self.token_endpoint.revoke_refresh_on_issue = True areq = AUTH_REQ.copy() @@ -852,6 +855,7 @@ def test_revoke_on_issue_refresh_token_per_client(self, conf): "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"] } self.endpoint_context.cdb[AUTH_REQ["client_id"]]["revoke_refresh_on_issue"] = True areq = AUTH_REQ.copy() @@ -1022,6 +1026,7 @@ def create_endpoint(self, conf): "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"] } endpoint_context.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") self.session_manager = endpoint_context.session_manager diff --git a/tests/test_server_50_persistence.py b/tests/test_server_50_persistence.py index 72171831..22a5bb51 100644 --- a/tests/test_server_50_persistence.py +++ b/tests/test_server_50_persistence.py @@ -215,6 +215,7 @@ def create_endpoint(self): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access", "research_and_scholarship"] } _store = server1.endpoint_context.dump() diff --git a/tests/test_server_60_dpop.py b/tests/test_server_60_dpop.py index a94d90b7..cd0301ef 100644 --- a/tests/test_server_60_dpop.py +++ b/tests/test_server_60_dpop.py @@ -189,6 +189,7 @@ def create_endpoint(self): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } self.user_id = "diana" self.token_endpoint = server.server_get("endpoint", "token") diff --git a/tests/test_server_61_add_on.py b/tests/test_server_61_add_on.py index 498b9e4d..366630eb 100644 --- a/tests/test_server_61_add_on.py +++ b/tests/test_server_61_add_on.py @@ -143,6 +143,7 @@ def create_endpoint(self): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"] } self.endpoint = server.server_get("endpoint", "authorization")