From 0e0e70ba149f87aa319ca3e8894b599f58dde349 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 25 Nov 2024 19:55:48 +0100 Subject: [PATCH 01/21] Calculating content_type Do metadata not provider_info Refactoring Claims --- src/idpyoidc/client/claims/__init__.py | 9 +- src/idpyoidc/client/claims/oauth2.py | 18 +- src/idpyoidc/client/claims/oauth2resource.py | 2 +- src/idpyoidc/client/claims/oidc.py | 14 +- src/idpyoidc/server/__init__.py | 18 ++ src/idpyoidc/server/claims/oauth2.py | 8 +- src/idpyoidc/server/endpoint.py | 154 +++++++++--------- src/idpyoidc/server/endpoint_context.py | 2 +- tests/test_client_41_rp_handler_persistent.py | 2 +- 9 files changed, 135 insertions(+), 92 deletions(-) diff --git a/src/idpyoidc/client/claims/__init__.py b/src/idpyoidc/client/claims/__init__.py index 1427005b..d495881b 100644 --- a/src/idpyoidc/client/claims/__init__.py +++ b/src/idpyoidc/client/claims/__init__.py @@ -13,6 +13,8 @@ def get_client_authn_methods(): class Claims(claims.Claims): + _supports = {} + def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): _base = configuration.get("base_url") if not _base: @@ -54,9 +56,10 @@ def get_jwks(self, keyjar): # if only one key under the id == "", that key being a SYMKey I assume it's # and I have a client_secret then don't publish a JWKS if ( - len(_own_keys) == 1 - and isinstance(_own_keys[0], SYMKey) - and self.prefer["client_secret"] + len(_own_keys) == 1 + and isinstance(_own_keys[0], SYMKey) + and self.prefer["client_secret"] + and self.prefer.get("client_secret", None) ): pass else: diff --git a/src/idpyoidc/client/claims/oauth2.py b/src/idpyoidc/client/claims/oauth2.py index 16e90475..543d619c 100644 --- a/src/idpyoidc/client/claims/oauth2.py +++ b/src/idpyoidc/client/claims/oauth2.py @@ -2,20 +2,34 @@ from idpyoidc.client import claims from idpyoidc.transform import create_registration_request +from idpyoidc.transform import create_registration_request +REGISTER2PREFERRED = { + "scope": "scopes_supported", + "token_endpoint_auth_signing_alg": "token_endpoint_auth_signing_alg_values_supported", + "response_types": "response_types_supported", + # "response_modes": "response_modes_supported", + "grant_types": "grant_types_supported", + "token_endpoint_auth_method": "token_endpoint_auth_methods_supported", + "token_auth_signing_algs": "token_auth_signing_algs_supported", + # 'ui_locales': 'ui_locales_supported', +} class Claims(claims.Claims): + register2preferred = REGISTER2PREFERRED + _supports = { "redirect_uris": None, "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], "response_types_supported": ["code"], "client_id": None, - "client_secret": None, "client_name": None, + "client_secret": None, "client_uri": None, "logo_uri": None, + "scope": None, "contacts": None, - "scopes_supported": [], + # "scopes_supported": [], "tos_uri": None, "policy_uri": None, "jwks_uri": None, diff --git a/src/idpyoidc/client/claims/oauth2resource.py b/src/idpyoidc/client/claims/oauth2resource.py index 537e1391..2bec3c8e 100644 --- a/src/idpyoidc/client/claims/oauth2resource.py +++ b/src/idpyoidc/client/claims/oauth2resource.py @@ -2,7 +2,7 @@ from idpyoidc.client import claims from idpyoidc.message.oauth2 import OAuthProtectedResourceRequest -from idpyoidc.client.claims.transform import array_or_singleton +from idpyoidc.transform import array_or_singleton class Claims(claims.Claims): _supports = { diff --git a/src/idpyoidc/client/claims/oidc.py b/src/idpyoidc/client/claims/oidc.py index d2b1b0b0..85cf9242 100644 --- a/src/idpyoidc/client/claims/oidc.py +++ b/src/idpyoidc/client/claims/oidc.py @@ -95,13 +95,13 @@ def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] client_claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) def verify_rules(self, supports): - if self.get_preference("request_parameter_supported") and self.get_preference( - "request_uri_parameter_supported" - ): - raise ValueError( - "You have to chose one of 'request_parameter_supported' and " - "'request_uri_parameter_supported'. You can't have both." - ) + # if self.get_preference("request_parameter_supported") and self.get_preference( + # "request_uri_parameter_supported" + # ): + # raise ValueError( + # "You have to chose one of 'request_parameter_supported' and " + # "'request_uri_parameter_supported'. You can't have both." + # ) if self.get_preference("request_parameter_supported") or self.get_preference( "request_uri_parameter_supported" diff --git a/src/idpyoidc/server/__init__.py b/src/idpyoidc/server/__init__.py index 78c2370b..b8c9671e 100644 --- a/src/idpyoidc/server/__init__.py +++ b/src/idpyoidc/server/__init__.py @@ -6,6 +6,7 @@ from typing import Union from cryptojwt import KeyJar +from cryptojwt.utils import importer from idpyoidc.client.defaults import DEFAULT_KEY_DEFS from idpyoidc.node import Unit @@ -52,6 +53,9 @@ def __init__( if _conf: self.entity_id = _conf.get("entity_id", "") self.issuer = conf.get("issuer", self.entity_id) + if not self.entity_id and self.issuer: + self.entity_id = self.issuer + self.persistence = None if upstream_get is None: @@ -95,6 +99,20 @@ def __init__( _token_endp = self.endpoint.get("token") + if isinstance(conf, dict): + metadata_schema = conf.get("metadata_schema", None) + else: + metadata_schema = conf.conf.get("metadata_schema", None) + + if metadata_schema: + metadata_schema = importer(metadata_schema) + self.context.provider_info = self.context.claims.get_server_metadata( + endpoints = self.endpoint.values(), + metadata_schema = metadata_schema, + ) + self.context.provider_info["issuer"] = self.issuer + self.context.metadata = self.context.provider_info + self.context.map_supported_to_preferred() if _token_endp: _token_endp.allow_refresh = allow_refresh_token(self.context) diff --git a/src/idpyoidc/server/claims/oauth2.py b/src/idpyoidc/server/claims/oauth2.py index 86e969df..243e09b3 100644 --- a/src/idpyoidc/server/claims/oauth2.py +++ b/src/idpyoidc/server/claims/oauth2.py @@ -1,5 +1,6 @@ from typing import Optional +from idpyoidc.message import Message from idpyoidc.message.oauth2 import ASConfigurationResponse from idpyoidc.server import claims @@ -38,9 +39,12 @@ class Claims(claims.Claims): def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] = None): claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) - def provider_info(self, supports): + def metadata(self, supports, schema: Optional[Message] = None): _info = {} - for key in ASConfigurationResponse.c_param.keys(): + if schema is None: + schema = ASConfigurationResponse + + for key in schema.c_param.keys(): _val = self.get_preference(key, supports.get(key, None)) if _val and _val != []: _info[key] = _val diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index 07c0080b..f23f9955 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -181,11 +181,11 @@ def verify_request(self, request, keyjar, client_id, verify_args, lap=0): return None def parse_request( - self, - request: Union[Message, dict, str], - http_info: Optional[dict] = None, - verify_args: Optional[dict] = None, - **kwargs + self, + request: Union[Message, dict, str], + http_info: Optional[dict] = None, + verify_args: Optional[dict] = None, + **kwargs ): """ @@ -195,8 +195,8 @@ def parse_request( :param kwargs: extra keyword arguments :return: """ - LOGGER.debug("- {} -".format(self.endpoint_name)) - LOGGER.info("Request: %s" % sanitize(request)) + LOGGER.debug(f"- {self.endpoint_name} -") + LOGGER.info(f"Request: {sanitize(request)}") _context = self.upstream_get("context") _keyjar = self.upstream_get("attribute", "keyjar") @@ -228,7 +228,6 @@ def parse_request( # Verify that the client is allowed to do this auth_info = self.client_authentication(req, http_info, endpoint=self, **kwargs) - LOGGER.debug(f"parse_request:auth_info:{auth_info}") _client_id = auth_info.get("client_id", "") if _client_id: @@ -274,17 +273,18 @@ def client_authentication(self, request: Message, http_info: Optional[dict] = No authn_info = verify_client(request=request, http_info=http_info, **kwargs) - LOGGER.debug("authn_info: %s", authn_info) + LOGGER.debug(f"authn_info: {authn_info}") if authn_info == {}: if self.client_authn_method and len(self.client_authn_method): - LOGGER.debug("client_authn_method: %s", self.client_authn_method) + LOGGER.debug(f"client_authn_method: {self.client_authn_method}") raise UnAuthorizedClient("Authorization failed") elif "client_id" not in authn_info and authn_info.get("method") != "none": + LOGGER.debug(f"No client ID") raise UnAuthorizedClient("Authorization failed") return authn_info def do_post_parse_request( - self, request: Message, client_id: Optional[str] = "", **kwargs + self, request: Message, client_id: Optional[str] = "", **kwargs ) -> Message: _context = self.upstream_get("context") for meth in self.post_parse_request: @@ -294,7 +294,7 @@ def do_post_parse_request( return request def do_pre_construct( - self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs + self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs ) -> dict: _context = self.upstream_get("context") for meth in self.pre_construct: @@ -303,10 +303,10 @@ def do_pre_construct( return response_args def do_post_construct( - self, - response_args: Union[Message, dict], - request: Optional[Union[Message, dict]] = None, - **kwargs + self, + response_args: Union[Message, dict], + request: Optional[Union[Message, dict]] = None, + **kwargs ) -> dict: _context = self.upstream_get("context") for meth in self.post_construct: @@ -315,10 +315,10 @@ def do_post_construct( return response_args def process_request( - self, - request: Optional[Union[Message, dict]] = None, - http_info: Optional[dict] = None, - **kwargs + self, + request: Optional[Union[Message, dict]] = None, + http_info: Optional[dict] = None, + **kwargs ) -> Union[Message, dict]: """ @@ -329,10 +329,10 @@ def process_request( return {} def construct( - self, - response_args: Optional[dict] = None, - request: Optional[Union[Message, dict]] = None, - **kwargs + self, + response_args: Optional[dict] = None, + request: Optional[Union[Message, dict]] = None, + **kwargs ): """ Construct the response @@ -350,19 +350,34 @@ def construct( return self.do_post_construct(response, request, **kwargs) def response_info( - self, - response_args: Optional[dict] = None, - request: Optional[Union[Message, dict]] = None, - **kwargs + self, + response_args: Optional[dict] = None, + request: Optional[Union[Message, dict]] = None, + **kwargs ) -> dict: return self.construct(response_args, request, **kwargs) + def _get_content_type(self, **kwargs): + content_type = kwargs.get("content_type", None) + + if content_type is None: + if self.response_content_type: + content_type = self.response_content_type + elif self.response_format == "json": + content_type = "application/json" + elif self.response_format in ["jws", "jwe", "jose"]: + content_type = "application/jose" + elif self.response_format == "text": + content_type = "text/plain" + else: + content_type = "application/x-www-form-urlencoded" + return content_type def do_response( - self, - response_args: Optional[dict] = None, - request: Optional[Union[Message, dict]] = None, - error: Optional[str] = "", - **kwargs + self, + response_args: Optional[dict] = None, + request: Optional[Union[Message, dict]] = None, + error: Optional[str] = "", + **kwargs ) -> dict: """ :param response_args: Information to use when constructing the response @@ -380,6 +395,7 @@ def do_response( resp = None if error: + content_type = "text/html" _response = ResponseMessage(error=error) for attr in ["error_description", "error_uri", "state"]: if attr in kwargs: @@ -389,58 +405,46 @@ def do_response( _response_placement = kwargs.get("response_placement") do_placement = False _response = "" - content_type = kwargs.get("content_type") - if content_type is None: - if self.response_content_type: - content_type = self.response_content_type - elif self.response_format == "json": - content_type = "application/json" - elif self.response_format in ["jws", "jwe", "jose"]: - content_type = "application/jose" - elif self.response_format == "text": - content_type = "text/plain" - else: - content_type = "application/x-www-form-urlencoded" + content_type = self._get_content_type(**kwargs) else: + content_type = "" _response = self.response_info(response_args, request, **kwargs) if do_placement: - content_type = kwargs.get("content_type") - if content_type is None: - if self.response_placement == "body": - if self.response_format == "json": + if not content_type: + content_type = self._get_content_type(**kwargs) + if self.response_placement == "body": + if self.response_format == "json": + if not content_type: content_type = "application/json; charset=utf-8" - if isinstance(_response, Message): - resp = _response.to_json() - else: - resp = json.dumps(_response) - elif self.response_format in ["jws", "jwe", "jose"]: - if self.response_content_type: - content_type = self.response_content_type - else: - content_type = "application/jose; charset=utf-8" - resp = _response + if isinstance(_response, Message): + resp = _response.to_json() else: + resp = json.dumps(_response) + elif self.response_format in ["jws", "jwe", "jose"]: + if not content_type: + content_type = "application/jose; charset=utf-8" + resp = _response + else: + if not content_type: content_type = "application/x-www-form-urlencoded" - resp = _response.to_urlencoded() - elif self.response_placement == "url": + resp = _response.to_urlencoded() + elif self.response_placement == "url": + if not content_type: content_type = "application/x-www-form-urlencoded" - fragment_enc = kwargs.get("fragment_enc") - if not fragment_enc: - _ret_type = kwargs.get("return_type") - if _ret_type: - fragment_enc = fragment_encoding(_ret_type) - else: - fragment_enc = False - - if fragment_enc: - resp = _response.request(kwargs["return_uri"], True) + + fragment_enc = kwargs.get("fragment_enc") + if not fragment_enc: + _ret_type = kwargs.get("return_type") + if _ret_type: + fragment_enc = fragment_encoding(_ret_type) else: - resp = _response.request(kwargs["return_uri"]) + fragment_enc = False + + if fragment_enc: + resp = _response.request(kwargs["return_uri"], True) else: - raise ValueError( - "Don't know where that is: '{}".format(self.response_placement) - ) + raise ValueError(f"Don't know where that is: '{self.response_placement}'") if content_type: try: diff --git a/src/idpyoidc/server/endpoint_context.py b/src/idpyoidc/server/endpoint_context.py index 3b46ef3e..dcfa3b69 100755 --- a/src/idpyoidc/server/endpoint_context.py +++ b/src/idpyoidc/server/endpoint_context.py @@ -240,7 +240,7 @@ def __init__( conf = conf.conf _supports = self.supports() self.keyjar = self.claims.load_conf(conf, supports=_supports, keyjar=keyjar) - self.provider_info = self.claims.provider_info(_supports) + self.provider_info = self.claims.metadata(_supports) self.provider_info["issuer"] = self.issuer self.provider_info.update(self._get_endpoint_info()) diff --git a/tests/test_client_41_rp_handler_persistent.py b/tests/test_client_41_rp_handler_persistent.py index 7cfb38e3..0a6eaf1a 100644 --- a/tests/test_client_41_rp_handler_persistent.py +++ b/tests/test_client_41_rp_handler_persistent.py @@ -292,7 +292,7 @@ def test_begin(self): assert query["client_id"] == ["eeeeeeeee"] assert query["redirect_uri"] == ["https://example.com/rp/authz_cb/github"] assert query["response_type"] == ["code"] - assert query["scope"] == ["user public_repo openid"] + assert query["scope"] == ["openid"] def test_get_session_information(self): rph_1 = RPHandler( From eb8cee055f4b18488bc3163c774706970f41af9a Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 25 Nov 2024 19:59:48 +0100 Subject: [PATCH 02/21] Patches --- patch/alg_info.patch | 1167 +++++++++ patch/claims.patch | 1370 ++++++++++ patch/metadata.patch | 63 + patch/oauth2.patch | 954 +++++++ patch/oidc.patch | 687 +++++ patch/test_client_30.patch | 158 ++ patch/tests.patch | 5084 ++++++++++++++++++++++++++++++++++++ patch/transform.patch | 60 + 8 files changed, 9543 insertions(+) create mode 100644 patch/alg_info.patch create mode 100644 patch/claims.patch create mode 100644 patch/metadata.patch create mode 100644 patch/oauth2.patch create mode 100644 patch/oidc.patch create mode 100644 patch/test_client_30.patch create mode 100644 patch/tests.patch create mode 100644 patch/transform.patch diff --git a/patch/alg_info.patch b/patch/alg_info.patch new file mode 100644 index 00000000..f15b7a2f --- /dev/null +++ b/patch/alg_info.patch @@ -0,0 +1,1167 @@ +diff --git a/src/idpyoidc/__init__.py b/src/idpyoidc/__init__.py +index 76d83a3..834b77a 100644 +--- a/src/idpyoidc/__init__.py ++++ b/src/idpyoidc/__init__.py +@@ -1,5 +1,5 @@ + __author__ = "Roland Hedberg" +-__version__ = "4.3.0" ++__version__ = "5.0.0" + + VERIFIED_CLAIM_PREFIX = "__verified" + +@@ -10,7 +10,7 @@ def verified_claim_name(claim): + + def proper_path(path): + """ +- Clean up the path specification so it looks like something I could use. ++ Clean up the path specification such that it looks like something I could use. + "./" "/" + """ + if path.startswith("./"): + +diff --git a/src/idpyoidc/alg_info.py b/src/idpyoidc/alg_info.py +new file mode 100644 +index 0000000..f3a9641 +--- /dev/null ++++ b/src/idpyoidc/alg_info.py +@@ -0,0 +1,67 @@ ++from functools import cmp_to_key ++import logging ++ ++from cryptojwt.jwe import DEPRECATED ++from cryptojwt.jwe import SUPPORTED ++from cryptojwt.jws.jws import SIGNER_ALGS ++ ++logger = logging.getLogger(__name__) ++ ++SIGNING_ALGORITHM_SORT_ORDER = ["RS", "ES", "PS", "HS", "Ed"] ++ ++ ++def cmp(a, b): ++ return (a > b) - (a < b) ++ ++ ++def alg_cmp(a, b): ++ if a == "none": ++ return 1 ++ elif b == "none": ++ return -1 ++ ++ _pos1 = SIGNING_ALGORITHM_SORT_ORDER.index(a[0:2]) ++ _pos2 = SIGNING_ALGORITHM_SORT_ORDER.index(b[0:2]) ++ if _pos1 == _pos2: ++ return (a > b) - (a < b) ++ elif _pos1 > _pos2: ++ return 1 ++ else: ++ return -1 ++ ++ ++def get_signing_algs(): ++ # Assumes Cryptojwt ++ _algs = [name for name in list(SIGNER_ALGS.keys()) if name != "none" and name not in DEPRECATED["alg"]] ++ return sorted(_algs, key=cmp_to_key(alg_cmp)) ++ ++ ++def get_encryption_algs(): ++ return SUPPORTED["alg"] ++ ++ ++def get_encryption_encs(): ++ return SUPPORTED["enc"] ++ ++ ++def array_or_singleton(claim_spec, values): ++ if isinstance(claim_spec[0], list): ++ if isinstance(values, list): ++ return values ++ else: ++ return [values] ++ else: ++ if isinstance(values, list): ++ return values[0] ++ else: # singleton ++ return values ++ ++ ++def is_subset(a, b): ++ if isinstance(a, list): ++ if isinstance(b, list): ++ return set(b).issubset(set(a)) ++ elif isinstance(b, list): ++ return a in b ++ else: ++ return a == b + +diff --git a/src/idpyoidc/claims.py b/src/idpyoidc/claims.py +index e684624..afa6680 100644 +--- a/src/idpyoidc/claims.py ++++ b/src/idpyoidc/claims.py +@@ -1,4 +1,6 @@ ++import logging + from typing import Callable ++from typing import List + from typing import Optional + + from cryptojwt import KeyJar +@@ -7,9 +9,14 @@ from cryptojwt.utils import importer + + from idpyoidc.client.util import get_uri + from idpyoidc.impexp import ImpExp ++from idpyoidc.key_import import import_jwks ++from idpyoidc.key_import import store_under_other_id ++from idpyoidc.message import Message ++from idpyoidc.transform import preferred_to_registered + from idpyoidc.util import add_path + from idpyoidc.util import qualified_name + ++logger = logging.getLogger(__name__) + + def claims_dump(info, exclude_attributes): + return {qualified_name(info.__class__): info.dump(exclude_attributes=exclude_attributes)} +@@ -85,7 +92,17 @@ class Claims(ImpExp): + self.callback = callbacks + + def verify_rules(self, supports): +- return True ++ if self.get_preference("encrypt_userinfo_supported", False) is True: ++ self.set_preference("userinfo_encryption_alg_values_supported", []) ++ self.set_preference("userinfo_encryption_enc_values_supported", []) ++ ++ if self.get_preference("encrypt_request_object_supported", False) is True: ++ self.set_preference("request_object_encryption_alg_values_supported", []) ++ self.set_preference("request_object_encryption_enc_values_supported", []) ++ ++ if self.get_preference("encrypt_id_token_supported", False) is True: ++ self.set_preference("id_token_encryption_alg_values_supported", []) ++ self.set_preference("id_token_encryption_enc_values_supported", []) + + def locals(self, info): + pass +@@ -104,11 +121,11 @@ class Claims(ImpExp): + else: + _keyjar = KeyJar() + if "jwks" in conf: +- _keyjar.import_jwks(conf["jwks"], "") ++ _keyjar = import_jwks(_keyjar, conf["jwks"], "") + + if "" in _keyjar and entity_id: + # make sure I have the keys under my own name too (if I know it) +- _keyjar.import_jwks_as_json(_keyjar.export_jwks_as_json(True, ""), entity_id) ++ _keyjar = store_under_other_id(_keyjar, "", entity_id, True) + + _httpc_params = conf.get("httpc_params") + if _httpc_params: +@@ -122,7 +139,7 @@ class Claims(ImpExp): + + return keyjar, _uri_path + +- def get_base_url(self, configuration: dict, entity_id: Optional[str]=""): ++ def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): + raise NotImplementedError() + + def get_id(self, configuration: dict): +@@ -138,6 +155,7 @@ class Claims(ImpExp): + configuration: dict, + keyjar: Optional[KeyJar] = None, + entity_id: Optional[str] = ""): ++ logger.debug(f"configuration: {configuration}") + _jwks = _jwks_uri = None + _id = self.get_id(configuration) + keyjar, uri_path = self._keyjar(keyjar, configuration, entity_id=_id) +@@ -180,6 +198,10 @@ class Claims(ImpExp): + elif val: + self.set_preference(key, val) + ++ for attr, val in supports.items(): ++ if attr not in self.prefer and val is not None: ++ self.set_preference(attr, val) ++ + self.verify_rules(supports) + return keyjar + +@@ -195,15 +217,21 @@ class Claims(ImpExp): + def construct_uris(self, *args): + pass + +- def supports(self): ++ def _expand(self, dictionary): + res = {} +- for key, val in self._supports.items(): ++ for key, val in dictionary.items(): + if isinstance(val, Callable): + res[key] = val() + else: +- res[key] = val ++ if isinstance(val, dict): ++ res[key] = self._expand(val) ++ else: ++ res[key] = val + return res + ++ def supports(self): ++ return self._expand(self._supports) ++ + def supported(self, claim): + return claim in self._supports + +@@ -219,3 +247,77 @@ class Claims(ImpExp): + return default + else: + return _val ++ ++ def get_endpoint_claims(self, endpoints): ++ _info = {} ++ for endp in endpoints: ++ if endp.endpoint_name: ++ _info[endp.endpoint_name] = endp.full_path ++ for arg, claim in [("client_authn_method", "auth_methods"), ++ ("auth_signing_alg_values", "auth_signing_alg_values")]: ++ _val = getattr(endp, arg, None) ++ if _val: ++ # trust_mark_status_endpoint_auth_methods_supported ++ md_param = f"{endp.endpoint_name}_{claim}" ++ _info[md_param] = _val ++ return _info ++ ++ def get_server_metadata(self, ++ entity_type: Optional[str] = "", ++ endpoints: Optional[list] = None, ++ metadata_schema: Optional[Message] = None, ++ extra_claims: Optional[List[str]] = None, ++ **kwargs): ++ ++ metadata = self.prefer ++ # the claims that can appear in the metadata ++ if metadata_schema: ++ attr = list(metadata_schema.c_param.keys()) ++ else: ++ attr = [] ++ ++ if extra_claims: ++ attr.extend(extra_claims) ++ ++ if attr: ++ metadata = {k: v for k, v in metadata.items() if k in attr and v != []} ++ ++ # collect endpoints ++ if endpoints: ++ metadata.update(self.get_endpoint_claims(endpoints)) ++ ++ if entity_type: ++ return {entity_type: metadata} ++ else: ++ return metadata ++ ++ def get_client_metadata(self, ++ entity_type: Optional[str] = "", ++ metadata_schema: Optional[Message] = None, ++ extra_claims: Optional[List[str]] = None, ++ supported: Optional[dict] = None, ++ **kwargs): ++ ++ if supported is None: ++ supported = self.supports() ++ ++ if not self.use: ++ self.use = preferred_to_registered(self.prefer, supported=supported) ++ ++ metadata = self.use ++ # the claims that can appear in the metadata ++ if metadata_schema: ++ attr = list(metadata_schema.c_param.keys()) ++ else: ++ attr = [] ++ ++ if extra_claims: ++ attr.extend(extra_claims) ++ ++ if attr: ++ metadata = {k: v for k, v in metadata.items() if k in attr} ++ ++ if entity_type: ++ return {entity_type: metadata} ++ else: ++ return metadata + +diff --git a/src/idpyoidc/client/entity_metadata.py b/src/idpyoidc/client/entity_metadata.py +new file mode 100644 +index 0000000..6b41b55 +--- /dev/null ++++ b/src/idpyoidc/client/entity_metadata.py +@@ -0,0 +1,36 @@ ++from typing import Optional ++ ++from idpyoidc.impexp import ImpExp ++ ++ ++class EntityMetadata(ImpExp): ++ parameter = {"metadata": {}} ++ def __init__(self, metadata: Optional[dict] = None): ++ ImpExp.__init__(self) ++ if metadata is None: ++ self.metadata = {} ++ else: ++ self.metadata = metadata ++ ++ def __getitem__(self, item): ++ for _type, _dict in self.metadata.items(): ++ _val = _dict.get(item, None) ++ if _val: ++ return _val ++ raise KeyError(item) ++ ++ def __setitem__(self, key, value): ++ # Assumes not multiple entity types ++ self.metadata[key] = value ++ ++ def items(self): ++ return self.metadata.items() ++ ++ def __contains__(self, item): ++ return item in self.metadata ++ ++ def get(self, item, default=None): ++ return self.metadata.get(item, default) ++ ++ def to_dict(self): ++ return self.metadata + + +diff --git a/src/idpyoidc/client/oauth2/access_token.py b/src/idpyoidc/client/oauth2/access_token.py +index 6ccb6f4..5161aa0 100644 +--- a/src/idpyoidc/client/oauth2/access_token.py ++++ b/src/idpyoidc/client/oauth2/access_token.py +@@ -7,7 +7,7 @@ from idpyoidc.client.oauth2.utils import get_state_parameter + from idpyoidc.client.service import Service + from idpyoidc.message import oauth2 + from idpyoidc.message.oauth2 import ResponseMessage +-from idpyoidc.metadata import get_signing_algs ++from idpyoidc.alg_info import get_signing_algs + from idpyoidc.time_util import time_sans_frac + + LOGGER = logging.getLogger(__name__) + +diff --git a/src/idpyoidc/client/provider/github.py b/src/idpyoidc/client/provider/github.py +index 56c9103..674a78d 100644 +--- a/src/idpyoidc/client/provider/github.py ++++ b/src/idpyoidc/client/provider/github.py +@@ -1,12 +1,12 @@ ++from idpyoidc.alg_info import get_signing_algs + from idpyoidc.client.client_auth import get_client_authn_methods + from idpyoidc.client.oauth2 import access_token + from idpyoidc.client.oidc import userinfo ++from idpyoidc.message import Message + from idpyoidc.message import SINGLE_OPTIONAL_STRING + from idpyoidc.message import SINGLE_REQUIRED_STRING +-from idpyoidc.message import Message + from idpyoidc.message import oauth2 + from idpyoidc.message.oauth2 import ResponseMessage +-from idpyoidc.metadata import get_signing_algs + + + class AccessTokenResponse(Message): +diff --git a/src/idpyoidc/client/provider/linkedin.py b/src/idpyoidc/client/provider/linkedin.py +index 17c7e85..e0bc430 100644 +--- a/src/idpyoidc/client/provider/linkedin.py ++++ b/src/idpyoidc/client/provider/linkedin.py +@@ -1,13 +1,13 @@ ++from idpyoidc.alg_info import get_signing_algs + from idpyoidc.client.client_auth import get_client_authn_methods + from idpyoidc.client.oauth2 import access_token + from idpyoidc.client.oidc import userinfo ++from idpyoidc.message import Message + from idpyoidc.message import SINGLE_OPTIONAL_JSON + from idpyoidc.message import SINGLE_OPTIONAL_STRING + from idpyoidc.message import SINGLE_REQUIRED_INT + from idpyoidc.message import SINGLE_REQUIRED_STRING +-from idpyoidc.message import Message + from idpyoidc.message import oauth2 +-from idpyoidc.metadata import get_signing_algs + + + class AccessTokenResponse(Message): + + +diff --git a/src/idpyoidc/client/rp_handler.py b/src/idpyoidc/client/rp_handler.py +index eea94c0..33d02f5 100644 +--- a/src/idpyoidc/client/rp_handler.py ++++ b/src/idpyoidc/client/rp_handler.py +@@ -3,19 +3,24 @@ import sys + import traceback + from typing import List + from typing import Optional ++from typing import Union + + from cryptojwt import KeyJar ++from cryptojwt.key_jar import build_keyjar + from cryptojwt.key_jar import init_key_jar + from cryptojwt.utils import as_bytes + from cryptojwt.utils import importer + ++from idpyoidc.client.configure import RPHConfiguration + from idpyoidc.client.defaults import DEFAULT_CLIENT_CONFIGS + from idpyoidc.client.defaults import DEFAULT_OIDC_SERVICES +-from idpyoidc.client.defaults import DEFAULT_RP_KEY_DEFS + from idpyoidc.client.oauth2.stand_alone_client import StandAloneClient ++from idpyoidc.configure import Base + from idpyoidc.util import add_path + from idpyoidc.util import rndstr ++from .defaults import DEFAULT_KEY_DEFS + from .oauth2 import Client ++from ..key_import import import_jwks + from ..message import Message + + logger = logging.getLogger(__name__) +@@ -34,50 +39,51 @@ class RPHandler(object): + state_db=None, + httpc=None, + httpc_params=None, +- config=None, ++ config: Optional[Union[dict, Base]] = None, + **kwargs, + ): +- self.base_url = base_url +- +- if keyjar is None: +- keyjar_defs = {} +- if config: +- keyjar_defs = getattr(config, "key_conf", None) +- +- if not keyjar_defs: +- keyjar_defs = kwargs.get("key_conf", DEFAULT_RP_KEY_DEFS) +- +- _jwks_path = kwargs.get("jwks_path", keyjar_defs.get("uri_path", keyjar_defs.get("public_path", ""))) +- if "uri_path" in keyjar_defs: +- del keyjar_defs["uri_path"] +- self.keyjar = init_key_jar(**keyjar_defs, issuer_id="") +- self.keyjar.import_jwks_as_json(self.keyjar.export_jwks_as_json(True, ""), base_url) +- else: ++ if config is None: ++ config = RPHConfiguration({}) ++ elif isinstance(config, dict): ++ config = RPHConfiguration(config) ++ ++ self.base_url = base_url or config.get("base_url", config.get("entity_id", "")) ++ self.entity_id = config.get("entity_id", config.conf.get("entity_id", self.base_url)) ++ self.entity_type = config.get("entity_type", config.conf.get("entity_type", "")) ++ self.client_type = config.get("client_type", config.conf.get("client_type", "")) ++ self.client_configs = client_configs or {} ++ ++ if keyjar: + self.keyjar = keyjar + _jwks_path = kwargs.get("jwks_path", "") +- +- if _jwks_path: +- self.jwks_uri = add_path(base_url, _jwks_path) +- else: +- self.jwks_uri = "" +- if len(self.keyjar): +- self.jwks = self.keyjar.export_jwks() ++ if _jwks_path: ++ self.jwks_uri = add_path(base_url, _jwks_path) + else: +- self.jwks = {} ++ self.jwks_uri = "" ++ if len(self.keyjar): ++ self.jwks = self.keyjar.export_jwks() ++ else: ++ self.jwks = {} + + if config: + if not hash_seed: + self.hash_seed = config.hash_seed +- if not keyjar: +- self.keyjar = init_key_jar(**config.key_conf, issuer_id="") +- if not client_configs: +- self.client_configs = config.clients +- +- if "client_class" in config: +- if isinstance(config["client_class"], str): +- self.client_cls = importer(config["client_class"]) ++ ++ if not keyjar and config.key_conf: ++ _conf = {k: v for k, v in config.key_conf.items() if k != "uri_path"} ++ self.keyjar = init_key_jar(**_conf, issuer_id="") ++ _jwks_path = kwargs.get("jwks_path", ++ config.key_conf.get("uri_path", ++ config.key_conf.get("public_path", ""))) ++ if _jwks_path: ++ self.jwks_uri = add_path(self.base_url, _jwks_path) ++ ++ _c_class = config.get("client_class", config.conf.get("client_class")) ++ if _c_class: ++ if isinstance(_c_class, str): ++ self.client_cls = importer(_c_class) + else: # assume it's a class +- self.client_cls = config["client_class"] ++ self.client_cls = _c_class + else: + self.client_cls = StandAloneClient + else: +@@ -86,23 +92,22 @@ class RPHandler(object): + else: + self.hash_seed = as_bytes(rndstr(32)) + +- if client_configs is None: +- self.client_configs = DEFAULT_CLIENT_CONFIGS +- for param in ["client_type", "preference", "add_ons"]: +- val = kwargs.get(param, None) +- if val: +- self.client_configs[""][param] = val +- else: +- self.client_configs = client_configs +- + _cc = kwargs.get("client_class", None) + if _cc: + if isinstance(_cc, str): + _cc = importer(_cc) +- self.client_cls =_cc ++ self.client_cls = _cc + else: + self.client_cls = StandAloneClient + ++ if client_configs is None: ++ self.client_configs = DEFAULT_CLIENT_CONFIGS ++ for param in ["client_type", "preference", "add_ons"]: ++ val = kwargs.get(param, None) ++ if val: ++ self.client_configs[""][param] = val ++ else: ++ self.client_configs = client_configs + + if state_db: + self.state_db = state_db +@@ -111,6 +116,9 @@ class RPHandler(object): + + self.extra = kwargs + ++ if services is None: ++ services = config.get("services", config.conf.get("services", None)) ++ + if services is None: + self.services = DEFAULT_OIDC_SERVICES + else: +@@ -122,15 +130,20 @@ class RPHandler(object): + self.httpc = httpc + + if not httpc_params: +- self.httpc_params = {"verify": verify_ssl} ++ self.httpc_params = config.get("httpc_params", {"verify": verify_ssl}) + else: + self.httpc_params = httpc_params + +- if not self.keyjar.httpc_params: +- self.keyjar.httpc_params = self.httpc_params +- + self.upstream_get = kwargs.get("upstream_get", None) + ++ _keyjar = getattr(self, "keyjar", None) ++ if _keyjar is not None: ++ if not _keyjar.httpc_params: ++ _keyjar.httpc_params = getattr(self, "httpc_params", {}) ++ else: ++ self.keyjar = build_keyjar(DEFAULT_KEY_DEFS) ++ self.keyjar.httpc_params = getattr(self, "httpc_params", {}) ++ + def state2issuer(self, state): + """ + Given the state value find the Issuer ID of the OP/AS that state value +@@ -159,7 +172,13 @@ class RPHandler(object): + :param issuer: Issuer ID + :return: A client configuration + """ +- return self.client_configs[issuer] ++ _cnf = self.client_configs[issuer].copy() ++ for param in ["entity_id", "client_id", "base_url", "services", "jwks_uri", "entity_type", ++ "client_type"]: ++ if param not in _cnf and getattr(self, param, None): ++ _cnf[param] = getattr(self, param) ++ ++ return _cnf + + def get_session_information(self, key, client=None): + """ +@@ -192,16 +211,7 @@ class RPHandler(object): + _cnf = self.pick_config("") + _cnf["issuer"] = issuer + +- try: +- _services = _cnf["services"] +- except KeyError: +- _services = self.services +- +- if "base_url" not in _cnf: +- _cnf["base_url"] = self.base_url +- +- if self.jwks_uri: +- _cnf["jwks_uri"] = self.jwks_uri ++ _services = _cnf["services"] + + logger.debug(f"config: {_cnf}") + try: +@@ -221,20 +231,29 @@ class RPHandler(object): + _context = client.get_context() + if _context.iss_hash: + self.hash2issuer[_context.iss_hash] = issuer ++ + # If non persistent + _keyjar = client.keyjar +- if not _keyjar: ++ if _keyjar is None: + _keyjar = KeyJar() + _keyjar.httpc_params.update(self.httpc_params) + +- for iss in self.keyjar.owners(): +- _keyjar.import_jwks(self.keyjar.export_jwks(issuer_id=iss, private=True), iss) ++ if self.upstream_get: ++ _srv_keyjar = self.upstream_get("attribute", "keyjar") ++ else: ++ _srv_keyjar = getattr(self, "keyjar", None) ++ ++ if _srv_keyjar: ++ for iss in _srv_keyjar.owners(): ++ _keyjar = import_jwks(_keyjar, self.keyjar.export_jwks(issuer_id=iss, private=True), iss) + + client.keyjar = _keyjar + # If persistent nothing has to be copied + +- _context.base_url = self.base_url +- _context.jwks_uri = self.jwks_uri ++ for item in ["jwks_uri", "base_url"]: ++ _val = getattr(self, item, None) ++ if _val: ++ setattr(_context, item, _val) + return client + + def do_provider_info( +@@ -639,7 +658,8 @@ class RPHandler(object): + return client.logout(state, post_logout_redirect_uri=post_logout_redirect_uri) + + def close( +- self, state: str, issuer: Optional[str] = "", post_logout_redirect_uri: Optional[str] = "" ++ self, state: str, issuer: Optional[str] = "", ++ post_logout_redirect_uri: Optional[str] = "" + ) -> dict: + + if issuer: + +diff --git a/src/idpyoidc/key_import.py b/src/idpyoidc/key_import.py +new file mode 100644 +index 0000000..9b33f50 +--- /dev/null ++++ b/src/idpyoidc/key_import.py +@@ -0,0 +1,76 @@ ++import json ++from typing import List ++from typing import Optional ++ ++from cryptojwt import JWK ++from cryptojwt import KeyBundle ++from cryptojwt import KeyJar ++from cryptojwt.jwk.hmac import SYMKey ++from cryptojwt.jwk.jwk import key_from_jwk_dict ++ ++ ++def issuer_keys(keyjar: KeyJar, entity_id: str, format: Optional[str] = "jwk"): ++ # sort of copying the functionality in KeyJar.get_issuer_keys() ++ key_issuer = keyjar.return_issuer(entity_id) ++ if format == "jwk": ++ return [k.serialize() for k in key_issuer.all_keys()] ++ else: ++ return [k for k in key_issuer.all_keys()] ++ ++ ++def import_jwks(keyjar: KeyJar, jwks: dict, entity_id: Optional[str] = "") -> KeyJar: ++ keys = [] ++ jar = issuer_keys(keyjar, entity_id) ++ for jwk in jwks["keys"]: ++ if jwk not in jar: ++ jar.append(jwk) ++ key = key_from_jwk_dict(jwk) ++ keys.append(key) ++ if keys: ++ keyjar.add_keys(entity_id, keys) ++ return keyjar ++ ++ ++def import_jwks_as_json(keyjar: KeyJar, jwks: str, entity_id: Optional[str] = "") -> KeyJar: ++ return import_jwks(keyjar, json.loads(jwks), entity_id) ++ ++ ++def import_jwks_from_file(keyjar: KeyJar, filename: str, entity_id) -> KeyJar: ++ with open(filename) as jwks_file: ++ keyjar = import_jwks_as_json(keyjar, jwks_file.read(), entity_id) ++ return keyjar ++ ++ ++def add_kb(keyjar: KeyJar, key_bundle: KeyBundle, entity_id: str) -> KeyJar: ++ return import_jwks(keyjar, json.loads(key_bundle.jwks()), entity_id) ++ ++ ++def add_symmetric(keyjar: KeyJar, key: str, entity_id: Optional[str] = "") -> KeyJar: ++ jar = issuer_keys(keyjar, entity_id) ++ _sym_key = SYMKey(key=key) ++ ++ jwk = _sym_key.serialize() ++ if jwk not in jar: ++ keyjar.add_symmetric(entity_id, key) ++ return keyjar ++ ++ ++def store_under_other_id(keyjar: KeyJar, fro: Optional[str] = "", to: Optional[str] = "", ++ private: Optional[bool] = False) -> KeyJar: ++ if fro == to: ++ return keyjar ++ else: ++ return import_jwks(keyjar, keyjar.export_jwks(private, fro), to) ++ ++ ++def add_keys(keyjar:KeyJar, keys: List[JWK], entity_id) -> KeyJar: ++ _keys = [] ++ jar = issuer_keys(keyjar, entity_id) ++ for key in keys: ++ jwk = key.serialize() ++ if jwk not in jar: ++ jar.append(jwk) ++ _keys.append(key) ++ if _keys: ++ keyjar.add_keys(entity_id, _keys) ++ return keyjar + + +diff --git a/src/idpyoidc/metadata.py b/src/idpyoidc/metadata.py +index 7561d48..e69de29 100644 +--- a/src/idpyoidc/metadata.py ++++ b/src/idpyoidc/metadata.py +@@ -1,274 +0,0 @@ +-from functools import cmp_to_key +-import logging +-from typing import Callable +-from typing import Optional +- +-from cryptojwt import KeyJar +-from cryptojwt.jwe import SUPPORTED +-from cryptojwt.jws.jws import SIGNER_ALGS +-from cryptojwt.key_jar import init_key_jar +-from cryptojwt.utils import importer +- +-from idpyoidc.client.util import get_uri +-from idpyoidc.impexp import ImpExp +-from idpyoidc.util import add_path +-from idpyoidc.util import qualified_name +- +-logger = logging.getLogger(__name__) +- +- +-def metadata_dump(info, exclude_attributes): +- return {qualified_name(info.__class__): info.dump(exclude_attributes=exclude_attributes)} +- +- +-def metadata_load(item: dict, **kwargs): +- _class_name = list(item.keys())[0] # there is only one +- _cls = importer(_class_name) +- _cls = _cls().load(item[_class_name]) +- return _cls +- +- +-class Metadata(ImpExp): +- parameter = {"prefer": None, "use": None, "callback_path": None, "_local": None} +- +- _supports = {} +- +- def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] = None): +- +- ImpExp.__init__(self) +- if isinstance(prefer, dict): +- self.prefer = {k: v for k, v in prefer.items() if k in self._supports} +- else: +- self.prefer = {} +- +- self.callback_path = callback_path or {} +- self.use = {} +- self._local = {} +- +- def get_use(self): +- return self.use +- +- def set_usage(self, key, value): +- self.use[key] = value +- +- def get_usage(self, key, default=None): +- return self.use.get(key, default) +- +- def get_preference(self, key, default=None): +- return self.prefer.get(key, default) +- +- def set_preference(self, key, value): +- self.prefer[key] = value +- +- def remove_preference(self, key): +- if key in self.prefer: +- del self.prefer[key] +- +- def _callback_uris(self, base_url, hex): +- _uri = [] +- for type in self.get_usage("response_types", self._supports["response_types"]): +- if "code" in type: +- _uri.append("code") +- elif type in ["id_token", "id_token token"]: +- _uri.append("implicit") +- +- if "form_post" in self.supports: +- _uri.append("form_post") +- +- callback_uri = {} +- for key in _uri: +- callback_uri[key] = get_uri(base_url, self.callback_path[key], hex) +- return callback_uri +- +- def construct_redirect_uris(self, base_url: str, hex: str, callbacks: Optional[dict] = None): +- if not callbacks: +- callbacks = self._callback_uris(base_url, hex) +- +- if callbacks: +- self.set_preference("callbacks", callbacks) +- self.set_preference("redirect_uris", [v for k, v in callbacks.items()]) +- +- self.callback = callbacks +- +- def verify_rules(self, supports): +- return True +- +- def locals(self, info): +- pass +- +- def _keyjar(self, keyjar=None, conf=None, entity_id=""): +- _uri_path = "" +- if keyjar is None: +- if "keys" in conf: +- keys_args = {k: v for k, v in conf["keys"].items() if k != "uri_path"} +- _keyjar = init_key_jar(**keys_args) +- _uri_path = conf["keys"].get("uri_path") +- elif "key_conf" in conf and conf["key_conf"]: +- keys_args = {k: v for k, v in conf["key_conf"].items() if k != "uri_path"} +- _keyjar = init_key_jar(**keys_args) +- _uri_path = conf["key_conf"].get("uri_path") +- else: +- _keyjar = KeyJar() +- if "jwks" in conf: +- _keyjar.import_jwks(conf["jwks"], "") +- +- if "" in _keyjar and entity_id: +- # make sure I have the keys under my own name too (if I know it) +- _keyjar.import_jwks_as_json(_keyjar.export_jwks_as_json(True, ""), entity_id) +- +- _httpc_params = conf.get("httpc_params") +- if _httpc_params: +- _keyjar.httpc_params = _httpc_params +- +- return _keyjar, _uri_path +- else: +- if "keys" in conf: +- _uri_path = conf["keys"].get("uri_path") +- elif "key_conf" in conf and conf["key_conf"]: +- _uri_path = conf["key_conf"].get("uri_path") +- return keyjar, _uri_path +- +- def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): +- raise NotImplementedError() +- +- def get_id(self, configuration: dict): +- raise NotImplementedError() +- +- def add_extra_keys(self, keyjar, id): +- return None +- +- def get_jwks(self, keyjar): +- return None +- +- def handle_keys(self, +- configuration: dict, +- keyjar: Optional[KeyJar] = None, +- base_url: Optional[str] = "", +- entity_id: Optional[str] = ""): +- _jwks = _jwks_uri = None +- _id = self.get_id(configuration) +- keyjar, uri_path = self._keyjar(keyjar, configuration, entity_id=_id) +- +- self.add_extra_keys(keyjar, _id) +- +- # now that keys are in the Key Jar, now for how to publish it +- if "jwks_uri" in configuration: # simple +- _jwks_uri = configuration.get("jwks_uri") +- elif uri_path: +- if not base_url: +- base_url = self.get_base_url(configuration, entity_id=entity_id) +- _jwks_uri = add_path(base_url, uri_path) +- else: # jwks or nothing +- _jwks = self.get_jwks(keyjar) +- +- return {"keyjar": keyjar, "jwks": _jwks, "jwks_uri": _jwks_uri} +- +- def load_conf( +- self, configuration, supports, keyjar: Optional[KeyJar] = None, +- base_url: Optional[str] = "" +- ): +- for attr, val in configuration.items(): +- if attr == "preference": +- for k, v in val.items(): +- if k in supports: +- self.set_preference(k, v) +- elif attr in supports: +- self.set_preference(attr, val) +- +- self.locals(configuration) +- +- for key, val in self.handle_keys(configuration, keyjar=keyjar, base_url=base_url).items(): +- if key == "keyjar": +- keyjar = val +- elif val: +- self.set_preference(key, val) +- +- self.verify_rules(supports) +- return keyjar +- +- def get(self, key, default=None): +- if key in self._local: +- return self._local[key] +- else: +- return default +- +- def set(self, key, val): +- self._local[key] = val +- +- def construct_uris(self, *args): +- pass +- +- def supports(self): +- res = {} +- for key, val in self._supports.items(): +- if isinstance(val, Callable): +- res[key] = val() +- else: +- res[key] = val +- return res +- +- def supported(self, claim): +- return claim in self._supports +- +- def prefers(self): +- return self.prefer +- +- +-SIGNING_ALGORITHM_SORT_ORDER = ["RS", "ES", "PS", "HS", "Ed"] +- +- +-def cmp(a, b): +- return (a > b) - (a < b) +- +- +-def alg_cmp(a, b): +- if a == "none": +- return 1 +- elif b == "none": +- return -1 +- +- _pos1 = SIGNING_ALGORITHM_SORT_ORDER.index(a[0:2]) +- _pos2 = SIGNING_ALGORITHM_SORT_ORDER.index(b[0:2]) +- if _pos1 == _pos2: +- return (a > b) - (a < b) +- elif _pos1 > _pos2: +- return 1 +- else: +- return -1 +- +- +-def get_signing_algs(): +- # Assumes Cryptojwt +- _algs = [name for name in list(SIGNER_ALGS.keys()) if name != "none"] +- return sorted(_algs, key=cmp_to_key(alg_cmp)) +- +- +-def get_encryption_algs(): +- return SUPPORTED["alg"] +- +- +-def get_encryption_encs(): +- return SUPPORTED["enc"] +- +- +-def array_or_singleton(claim_spec, values): +- if isinstance(claim_spec[0], list): +- if isinstance(values, list): +- return values +- else: +- return [values] +- else: +- if isinstance(values, list): +- return values[0] +- else: # singleton +- return values +- +- +-def is_subset(a, b): +- if isinstance(a, list): +- if isinstance(b, list): +- return set(b).issubset(set(a)) +- elif isinstance(b, list): +- return a in b +- else: +- return a == b + +diff --git a/src/idpyoidc/node.py b/src/idpyoidc/node.py +index 0db622a..2b64d6e 100644 +--- a/src/idpyoidc/node.py ++++ b/src/idpyoidc/node.py +@@ -7,14 +7,17 @@ from cryptojwt.key_jar import init_key_jar + + from idpyoidc.configure import Configuration + from idpyoidc.impexp import ImpExp ++from idpyoidc.key_import import import_jwks ++from idpyoidc.key_import import import_jwks_as_json ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.util import instantiate + + + def create_keyjar( +- keyjar: Optional[KeyJar] = None, +- conf: Optional[Union[dict, Configuration]] = None, +- key_conf: Optional[dict] = None, +- id: Optional[str] = "", ++ keyjar: Optional[KeyJar] = None, ++ conf: Optional[Union[dict, Configuration]] = None, ++ key_conf: Optional[dict] = None, ++ id: Optional[str] = "", + ): + if keyjar is None: + if key_conf: +@@ -30,13 +33,13 @@ def create_keyjar( + else: + _keyjar = KeyJar() + if "jwks" in conf: +- _keyjar.import_jwks(conf["jwks"], "") ++ _keyjar = import_jwks(_keyjar, conf["jwks"], "") + else: + _keyjar = None + + if _keyjar and "" in _keyjar and id: + # make sure I have the keys under my own name too (if I know it) +- _keyjar.import_jwks_as_json(_keyjar.export_jwks_as_json(True, ""), id) ++ _keyjar = store_under_other_id(_keyjar, "", id, True) + + return _keyjar + else: +@@ -60,7 +63,7 @@ def make_keyjar( + keyjar = KeyJar() + _jwks = config.get("jwks") + if _jwks: +- keyjar.import_jwks_as_json(_jwks, client_id) ++ keyjar = import_jwks_as_json(keyjar, _jwks, client_id) + + if keyjar or key_conf: + # Should be either one +@@ -78,15 +81,12 @@ def make_keyjar( + keyjar = KeyJar() + keyjar.add_symmetric(client_id, _key) + keyjar.add_symmetric("", _key) +- # else: +- # keyjar = build_keyjar(DEFAULT_KEY_DEFS) +- # if issuer_id: +- # keyjar.import_jwks(keyjar.export_jwks(private=True), issuer_id) + + return keyjar + + + class Node: ++ + def __init__(self, upstream_get: Callable = None): + self.upstream_get = upstream_get + +@@ -123,19 +123,20 @@ class Unit(ImpExp): + init_args = ["upstream_get"] + + def __init__( +- self, +- upstream_get: Callable = None, +- keyjar: Optional[Union[KeyJar, bool]] = None, +- httpc: Optional[object] = None, +- httpc_params: Optional[dict] = None, +- config: Optional[Union[Configuration, dict]] = None, +- key_conf: Optional[dict] = None, +- issuer_id: Optional[str] = "", +- client_id: Optional[str] = "", ++ self, ++ upstream_get: Callable = None, ++ keyjar: Optional[Union[KeyJar, bool]] = None, ++ httpc: Optional[object] = None, ++ httpc_params: Optional[dict] = None, ++ config: Optional[Union[Configuration, dict]] = None, ++ key_conf: Optional[dict] = None, ++ issuer_id: Optional[str] = "", ++ client_id: Optional[str] = "", + ): + ImpExp.__init__(self) + self.upstream_get = upstream_get + self.httpc = httpc ++ self.client_id = client_id + + if config is None: + config = {} +@@ -192,16 +193,16 @@ class ClientUnit(Unit): + name = "" + + def __init__( +- self, +- upstream_get: Callable = None, +- httpc: Optional[object] = None, +- httpc_params: Optional[dict] = None, +- keyjar: Optional[KeyJar] = None, +- context: Optional[ImpExp] = None, +- config: Optional[Union[Configuration, dict]] = None, +- # jwks_uri: Optional[str] = "", +- entity_id: Optional[str] = "", +- key_conf: Optional[dict] = None, ++ self, ++ upstream_get: Callable = None, ++ httpc: Optional[object] = None, ++ httpc_params: Optional[dict] = None, ++ keyjar: Optional[KeyJar] = None, ++ context: Optional[ImpExp] = None, ++ config: Optional[Union[Configuration, dict]] = None, ++ # jwks_uri: Optional[str] = "", ++ entity_id: Optional[str] = "", ++ key_conf: Optional[dict] = None, + ): + if config is None: + config = {} +@@ -232,17 +233,18 @@ class ClientUnit(Unit): + + # Neither client nor Server + class Collection(Unit): ++ + def __init__( +- self, +- upstream_get: Callable = None, +- keyjar: Optional[KeyJar] = None, +- httpc: Optional[object] = None, +- httpc_params: Optional[dict] = None, +- config: Optional[Union[Configuration, dict]] = None, +- entity_id: Optional[str] = "", +- key_conf: Optional[dict] = None, +- functions: Optional[dict] = None, +- claims: Optional[dict] = None, ++ self, ++ upstream_get: Callable = None, ++ keyjar: Optional[KeyJar] = None, ++ httpc: Optional[object] = None, ++ httpc_params: Optional[dict] = None, ++ config: Optional[Union[Configuration, dict]] = None, ++ entity_id: Optional[str] = "", ++ key_conf: Optional[dict] = None, ++ functions: Optional[dict] = None, ++ claims: Optional[dict] = None, + ): + if config is None: + config = {} + diff --git a/patch/claims.patch b/patch/claims.patch new file mode 100644 index 00000000..b0c2cd45 --- /dev/null +++ b/patch/claims.patch @@ -0,0 +1,1370 @@ + +diff --git a/src/idpyoidc/client/claims/__init__.py b/src/idpyoidc/client/claims/__init__.py +index 1427005..a13f25d 100644 +--- a/src/idpyoidc/client/claims/__init__.py ++++ b/src/idpyoidc/client/claims/__init__.py +@@ -13,6 +13,8 @@ def get_client_authn_methods(): + + + class Claims(claims.Claims): ++ _supports = {} ++ + def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): + _base = configuration.get("base_url") + if not _base: +@@ -56,7 +58,7 @@ class Claims(claims.Claims): + if ( + len(_own_keys) == 1 + and isinstance(_own_keys[0], SYMKey) +- and self.prefer["client_secret"] ++ and self.prefer.get("client_secret", None) + ): + pass + else: +diff --git a/src/idpyoidc/client/claims/oauth2.py b/src/idpyoidc/client/claims/oauth2.py +index 9d093d4..f4cf8d9 100644 +--- a/src/idpyoidc/client/claims/oauth2.py ++++ b/src/idpyoidc/client/claims/oauth2.py +@@ -1,21 +1,38 @@ + from typing import Optional + + from idpyoidc.client import claims +-from idpyoidc.client.claims.transform import create_registration_request ++from idpyoidc.transform import create_registration_request ++ ++REGISTER2PREFERRED = { ++ "scope": "scopes_supported", ++ "token_endpoint_auth_signing_alg": "token_endpoint_auth_signing_alg_values_supported", ++ "response_types": "response_types_supported", ++ # "response_modes": "response_modes_supported", ++ "grant_types": "grant_types_supported", ++ "token_endpoint_auth_method": "token_endpoint_auth_methods_supported", ++ "token_auth_signing_algs": "token_auth_signing_algs_supported", ++ # 'ui_locales': 'ui_locales_supported', ++} + + + class Claims(claims.Claims): ++ register2preferred = REGISTER2PREFERRED ++ + _supports = { + "redirect_uris": None, +- "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], ++ # "scopes_supported": [], + "response_types_supported": ["code"], ++ # "response_modes_supported": ["query", "fragment"], ++ "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], ++ "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"], ++ # "token_auth_signing_algs_supported": metadata.get_signing_algs(), + "client_id": None, +- "client_secret": None, + "client_name": None, ++ "client_secret": None, + "client_uri": None, + "logo_uri": None, ++ "scope": None, + "contacts": None, +- "scopes_supported": [], + "tos_uri": None, + "policy_uri": None, + "jwks_uri": None, + + +diff --git a/src/idpyoidc/client/claims/oauth2resource.py b/src/idpyoidc/client/claims/oauth2resource.py +index 537e139..2bec3c8 100644 +--- a/src/idpyoidc/client/claims/oauth2resource.py ++++ b/src/idpyoidc/client/claims/oauth2resource.py +@@ -2,7 +2,7 @@ from typing import Optional + + from idpyoidc.client import claims + from idpyoidc.message.oauth2 import OAuthProtectedResourceRequest +-from idpyoidc.client.claims.transform import array_or_singleton ++from idpyoidc.transform import array_or_singleton + + class Claims(claims.Claims): + _supports = { + + + +diff --git a/src/idpyoidc/client/claims/oidc.py b/src/idpyoidc/client/claims/oidc.py +index 0529f16..d8ae08b 100644 +--- a/src/idpyoidc/client/claims/oidc.py ++++ b/src/idpyoidc/client/claims/oidc.py +@@ -2,9 +2,9 @@ import logging + import os + from typing import Optional + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.client import claims as client_claims +-from idpyoidc.client.claims.transform import create_registration_request ++from idpyoidc.transform import create_registration_request + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + from idpyoidc.message.oidc import RegistrationRequest + from idpyoidc.message.oidc import RegistrationResponse +@@ -71,14 +71,15 @@ class Claims(client_claims.Claims): + "client_name": None, + "client_secret": None, + "client_uri": None, ++ "code_challenge_methods_supported": None, + "contacts": None, + "default_max_age": 86400, + "encrypt_id_token_supported": None, + # "grant_types_supported": ["authorization_code", "refresh_token"], + "logo_uri": None, +- "id_token_signing_alg_values_supported": metadata.get_signing_algs(), +- "id_token_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "id_token_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "id_token_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "id_token_encryption_alg_values_supported": alg_info.get_encryption_algs(), ++ "id_token_encryption_enc_values_supported": alg_info.get_encryption_encs(), + "initiate_login_uri": None, + "jwks": None, + "jwks_uri": None, +@@ -95,13 +96,13 @@ class Claims(client_claims.Claims): + client_claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) + + def verify_rules(self, supports): +- if self.get_preference("request_parameter_supported") and self.get_preference( +- "request_uri_parameter_supported" +- ): +- raise ValueError( +- "You have to chose one of 'request_parameter_supported' and " +- "'request_uri_parameter_supported'. You can't have both." +- ) ++ # if self.get_preference("request_parameter_supported") and self.get_preference( ++ # "request_uri_parameter_supported" ++ # ): ++ # raise ValueError( ++ # "You have to chose one of 'request_parameter_supported' and " ++ # "'request_uri_parameter_supported'. You can't have both." ++ # ) + + if self.get_preference("request_parameter_supported") or self.get_preference( + "request_uri_parameter_supported" + +diff --git a/src/idpyoidc/server/__init__.py b/src/idpyoidc/server/__init__.py +index 78c2370..9657257 100644 +--- a/src/idpyoidc/server/__init__.py ++++ b/src/idpyoidc/server/__init__.py +@@ -6,6 +6,7 @@ from typing import Optional + from typing import Union + + from cryptojwt import KeyJar ++from cryptojwt.utils import importer + + from idpyoidc.client.defaults import DEFAULT_KEY_DEFS + from idpyoidc.node import Unit +@@ -52,6 +53,9 @@ class Server(Unit): + if _conf: + self.entity_id = _conf.get("entity_id", "") + self.issuer = conf.get("issuer", self.entity_id) ++ if not self.entity_id and self.issuer: ++ self.entity_id = self.issuer ++ + self.persistence = None + + if upstream_get is None: +@@ -95,6 +99,19 @@ class Server(Unit): + + _token_endp = self.endpoint.get("token") + ++ if isinstance(conf, dict): ++ metadata_schema = conf.get("metadata_schema", None) ++ else: ++ metadata_schema = conf.conf.get("metadata_schema", None) ++ if metadata_schema: ++ metadata_schema = importer(metadata_schema) ++ self.context.provider_info = self.context.claims.get_server_metadata( ++ endpoints=self.endpoint.values(), ++ metadata_schema=metadata_schema, ++ ) ++ self.context.provider_info["issuer"] = self.issuer ++ self.context.metadata = self.context.provider_info ++ + self.context.map_supported_to_preferred() + if _token_endp: + _token_endp.allow_refresh = allow_refresh_token(self.context) +diff --git a/src/idpyoidc/server/claims/oauth2.py b/src/idpyoidc/server/claims/oauth2.py +index 86e969d..243e09b 100644 +--- a/src/idpyoidc/server/claims/oauth2.py ++++ b/src/idpyoidc/server/claims/oauth2.py +@@ -1,5 +1,6 @@ + from typing import Optional + ++from idpyoidc.message import Message + from idpyoidc.message.oauth2 import ASConfigurationResponse + from idpyoidc.server import claims + +@@ -38,9 +39,12 @@ class Claims(claims.Claims): + def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] = None): + claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) + +- def provider_info(self, supports): ++ def metadata(self, supports, schema: Optional[Message] = None): + _info = {} +- for key in ASConfigurationResponse.c_param.keys(): ++ if schema is None: ++ schema = ASConfigurationResponse ++ ++ for key in schema.c_param.keys(): + _val = self.get_preference(key, supports.get(key, None)) + if _val and _val != []: + _info[key] = _val +diff --git a/src/idpyoidc/server/claims/oidc.py b/src/idpyoidc/server/claims/oidc.py +index 2c258ba..0646410 100644 +--- a/src/idpyoidc/server/claims/oidc.py ++++ b/src/idpyoidc/server/claims/oidc.py +@@ -1,6 +1,7 @@ + from typing import Optional + +-from idpyoidc import metadata ++from idpyoidc import alg_info ++from idpyoidc.message import Message + from idpyoidc.message.oidc import ProviderConfigurationResponse + from idpyoidc.message.oidc import RegistrationRequest + from idpyoidc.message.oidc import RegistrationResponse +@@ -48,9 +49,9 @@ class Claims(server_claims.Claims): + "display_values_supported": None, + "encrypt_id_token_supported": None, + # "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], +- "id_token_signing_alg_values_supported": metadata.get_signing_algs(), +- "id_token_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "id_token_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "id_token_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "id_token_encryption_alg_values_supported": alg_info.get_encryption_algs(), ++ "id_token_encryption_enc_values_supported": alg_info.get_encryption_encs(), + "initiate_login_uri": None, + "jwks": None, + "jwks_uri": None, +@@ -71,13 +72,13 @@ class Claims(server_claims.Claims): + server_claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) + + def verify_rules(self, supports): +- if self.get_preference("request_parameter_supported") and self.get_preference( +- "request_uri_parameter_supported" +- ): +- raise ValueError( +- "You have to chose one of 'request_parameter_supported' and " +- "'request_uri_parameter_supported'. You can't have both." +- ) ++ # if self.get_preference("request_parameter_supported") and self.get_preference( ++ # "request_uri_parameter_supported" ++ # ): ++ # raise ValueError( ++ # "You have to chose one of 'request_parameter_supported' and " ++ # "'request_uri_parameter_supported'. You can't have both." ++ # ) + + if not self.get_preference("encrypt_userinfo_supported"): + self.set_preference("userinfo_encryption_alg_values_supported", []) +@@ -91,7 +92,7 @@ class Claims(server_claims.Claims): + self.set_preference("id_token_encryption_alg_values_supported", []) + self.set_preference("id_token_encryption_enc_values_supported", []) + +- def provider_info(self, supports): ++ def provider_info(self, supports, schema: Optional[Message] = None): + _info = {} + for key in ProviderConfigurationResponse.c_param.keys(): + _val = self.get_preference(key, supports.get(key, None)) + + +diff --git a/src/idpyoidc/server/configure.py b/src/idpyoidc/server/configure.py +index 3f304e7..bb4b507 100755 +--- a/src/idpyoidc/server/configure.py ++++ b/src/idpyoidc/server/configure.py +@@ -83,7 +83,7 @@ _C = { + "client_authn_method": None, + "claims_parameter_supported": True, + "request_parameter_supported": True, +- "request_uri_parameter_supported": True, ++ "request_uri_parameter_supported": None, + "response_types_supported": ["code"], + "response_modes_supported": ["query", "fragment", "form_post"], + }, +@@ -152,7 +152,7 @@ OP_DEFAULT_CONFIG.update( + "client_authn_method": None, + "claims_parameter_supported": True, + "request_parameter_supported": True, +- "request_uri_parameter_supported": True, ++ "request_uri_parameter_supported": None, + "response_types_supported": [ + "code", + # "token", +@@ -480,7 +480,7 @@ DEFAULT_EXTENDED_CONF = { + "client_authn_method": None, + "claims_parameter_supported": True, + "request_parameter_supported": True, +- "request_uri_parameter_supported": True, ++ "request_uri_parameter_supported": None, + "response_types_supported": [ + "code", + # "token", +diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py +index 07c0080..56074ad 100755 +--- a/src/idpyoidc/server/endpoint.py ++++ b/src/idpyoidc/server/endpoint.py +@@ -181,11 +181,11 @@ class Endpoint(Node): + return None + + def parse_request( +- self, +- request: Union[Message, dict, str], +- http_info: Optional[dict] = None, +- verify_args: Optional[dict] = None, +- **kwargs ++ self, ++ request: Union[Message, dict, str], ++ http_info: Optional[dict] = None, ++ verify_args: Optional[dict] = None, ++ **kwargs + ): + """ + +@@ -196,7 +196,9 @@ class Endpoint(Node): + :return: + """ + LOGGER.debug("- {} -".format(self.endpoint_name)) +- LOGGER.info("Request: %s" % sanitize(request)) ++ LOGGER.info(f"Request: {sanitize(request)}") ++ if http_info: ++ LOGGER.info(f"HTTP info: {http_info}") + + _context = self.upstream_get("context") + _keyjar = self.upstream_get("attribute", "keyjar") +@@ -240,8 +242,6 @@ class Endpoint(Node): + else: + _client_id = req.get("client_id", None) + +- LOGGER.debug(f"parse_request:auth_info:{auth_info}") +- + # verify that the request message is correct, may have to do it twice + err_response = self.verify_request( + request=req, keyjar=_keyjar, client_id=_client_id, verify_args=verify_args +@@ -274,17 +274,18 @@ class Endpoint(Node): + + authn_info = verify_client(request=request, http_info=http_info, **kwargs) + +- LOGGER.debug("authn_info: %s", authn_info) ++ LOGGER.debug(f"authn_info: {authn_info}") + if authn_info == {}: + if self.client_authn_method and len(self.client_authn_method): +- LOGGER.debug("client_authn_method: %s", self.client_authn_method) ++ LOGGER.debug(f"client_authn_method: {self.client_authn_method}") + raise UnAuthorizedClient("Authorization failed") + elif "client_id" not in authn_info and authn_info.get("method") != "none": ++ LOGGER.debug(f"No client ID") + raise UnAuthorizedClient("Authorization failed") + return authn_info + + def do_post_parse_request( +- self, request: Message, client_id: Optional[str] = "", **kwargs ++ self, request: Message, client_id: Optional[str] = "", **kwargs + ) -> Message: + _context = self.upstream_get("context") + for meth in self.post_parse_request: +@@ -294,7 +295,7 @@ class Endpoint(Node): + return request + + def do_pre_construct( +- self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs ++ self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs + ) -> dict: + _context = self.upstream_get("context") + for meth in self.pre_construct: +@@ -303,10 +304,10 @@ class Endpoint(Node): + return response_args + + def do_post_construct( +- self, +- response_args: Union[Message, dict], +- request: Optional[Union[Message, dict]] = None, +- **kwargs ++ self, ++ response_args: Union[Message, dict], ++ request: Optional[Union[Message, dict]] = None, ++ **kwargs + ) -> dict: + _context = self.upstream_get("context") + for meth in self.post_construct: +@@ -315,10 +316,10 @@ class Endpoint(Node): + return response_args + + def process_request( +- self, +- request: Optional[Union[Message, dict]] = None, +- http_info: Optional[dict] = None, +- **kwargs ++ self, ++ request: Optional[Union[Message, dict]] = None, ++ http_info: Optional[dict] = None, ++ **kwargs + ) -> Union[Message, dict]: + """ + +@@ -329,10 +330,10 @@ class Endpoint(Node): + return {} + + def construct( +- self, +- response_args: Optional[dict] = None, +- request: Optional[Union[Message, dict]] = None, +- **kwargs ++ self, ++ response_args: Optional[dict] = None, ++ request: Optional[Union[Message, dict]] = None, ++ **kwargs + ): + """ + Construct the response +@@ -350,19 +351,34 @@ class Endpoint(Node): + return self.do_post_construct(response, request, **kwargs) + + def response_info( +- self, +- response_args: Optional[dict] = None, +- request: Optional[Union[Message, dict]] = None, +- **kwargs ++ self, ++ response_args: Optional[dict] = None, ++ request: Optional[Union[Message, dict]] = None, ++ **kwargs + ) -> dict: + return self.construct(response_args, request, **kwargs) + ++ def _get_content_type(self, **kwargs): ++ content_type = kwargs.get("content_type", None) ++ if content_type is None: ++ if self.response_content_type: ++ content_type = self.response_content_type ++ elif self.response_format == "json": ++ content_type = "application/json" ++ elif self.response_format in ["jws", "jwe", "jose"]: ++ content_type = "application/jose" ++ elif self.response_format == "text": ++ content_type = "text/plain" ++ else: ++ content_type = "application/x-www-form-urlencoded" ++ return content_type ++ + def do_response( +- self, +- response_args: Optional[dict] = None, +- request: Optional[Union[Message, dict]] = None, +- error: Optional[str] = "", +- **kwargs ++ self, ++ response_args: Optional[dict] = None, ++ request: Optional[Union[Message, dict]] = None, ++ error: Optional[str] = "", ++ **kwargs + ) -> dict: + """ + :param response_args: Information to use when constructing the response +@@ -370,7 +386,6 @@ class Endpoint(Node): + :param error: Possible error encountered while processing the request + """ + do_placement = True +- content_type = "text/html" + _resp = {} + _response_placement = None + if response_args is None: +@@ -380,6 +395,7 @@ class Endpoint(Node): + + resp = None + if error: ++ content_type = "text/html" + _response = ResponseMessage(error=error) + for attr in ["error_description", "error_uri", "state"]: + if attr in kwargs: +@@ -389,58 +405,50 @@ class Endpoint(Node): + _response_placement = kwargs.get("response_placement") + do_placement = False + _response = "" +- content_type = kwargs.get("content_type") +- if content_type is None: +- if self.response_content_type: +- content_type = self.response_content_type +- elif self.response_format == "json": +- content_type = "application/json" +- elif self.response_format in ["jws", "jwe", "jose"]: +- content_type = "application/jose" +- elif self.response_format == "text": +- content_type = "text/plain" +- else: +- content_type = "application/x-www-form-urlencoded" ++ content_type = self._get_content_type(**kwargs) + else: ++ content_type = "" + _response = self.response_info(response_args, request, **kwargs) + + if do_placement: +- content_type = kwargs.get("content_type") +- if content_type is None: +- if self.response_placement == "body": +- if self.response_format == "json": ++ if not content_type: ++ content_type = self._get_content_type(**kwargs) ++ ++ if self.response_placement == "body": ++ if self.response_format == "json": ++ if not content_type: + content_type = "application/json; charset=utf-8" +- if isinstance(_response, Message): +- resp = _response.to_json() +- else: +- resp = json.dumps(_response) +- elif self.response_format in ["jws", "jwe", "jose"]: +- if self.response_content_type: +- content_type = self.response_content_type +- else: +- content_type = "application/jose; charset=utf-8" +- resp = _response ++ if isinstance(_response, Message): ++ resp = _response.to_json() + else: ++ resp = json.dumps(_response) ++ elif self.response_format in ["jws", "jwe", "jose"]: ++ if not content_type: ++ content_type = "application/jose; charset=utf-8" ++ resp = _response ++ else: ++ if not content_type: + content_type = "application/x-www-form-urlencoded" +- resp = _response.to_urlencoded() +- elif self.response_placement == "url": ++ resp = _response.to_urlencoded() ++ elif self.response_placement == "url": ++ if not content_type: + content_type = "application/x-www-form-urlencoded" +- fragment_enc = kwargs.get("fragment_enc") +- if not fragment_enc: +- _ret_type = kwargs.get("return_type") +- if _ret_type: +- fragment_enc = fragment_encoding(_ret_type) +- else: +- fragment_enc = False +- +- if fragment_enc: +- resp = _response.request(kwargs["return_uri"], True) ++ fragment_enc = kwargs.get("fragment_enc") ++ if not fragment_enc: ++ _ret_type = kwargs.get("return_type") ++ if _ret_type: ++ fragment_enc = fragment_encoding(_ret_type) + else: +- resp = _response.request(kwargs["return_uri"]) ++ fragment_enc = False ++ ++ if fragment_enc: ++ resp = _response.request(kwargs["return_uri"], True) + else: +- raise ValueError( +- "Don't know where that is: '{}".format(self.response_placement) +- ) ++ resp = _response.request(kwargs["return_uri"]) ++ else: ++ raise ValueError( ++ "Don't know where that is: '{}".format(self.response_placement) ++ ) + + if content_type: + try: +diff --git a/src/idpyoidc/server/endpoint_context.py b/src/idpyoidc/server/endpoint_context.py +index 3b46ef3..ac21db6 100755 +--- a/src/idpyoidc/server/endpoint_context.py ++++ b/src/idpyoidc/server/endpoint_context.py +@@ -11,6 +11,7 @@ from jinja2 import FileSystemLoader + from requests import request + + from idpyoidc.context import OidcContext ++from idpyoidc.message import Message + from idpyoidc.server import authz + from idpyoidc.server.claims import Claims + from idpyoidc.server.claims.oauth2 import Claims as OAUTH2_Claims +@@ -173,6 +174,7 @@ class EndpointContext(OidcContext): + self.token_args_methods = [] + self.userinfo = None + self.client_authn_method = {} ++ self.client_known_as = {} + + for param in [ + "issuer", +@@ -186,8 +188,6 @@ class EndpointContext(OidcContext): + except KeyError: + pass + +- self.token_handler_args = get_token_handler_args(conf) +- + # session db + self._sub_func = {} + self.do_sub_func() +@@ -240,9 +240,6 @@ class EndpointContext(OidcContext): + conf = conf.conf + _supports = self.supports() + self.keyjar = self.claims.load_conf(conf, supports=_supports, keyjar=keyjar) +- self.provider_info = self.claims.provider_info(_supports) +- self.provider_info["issuer"] = self.issuer +- self.provider_info.update(self._get_endpoint_info()) + + # INTERFACES + +@@ -250,23 +247,33 @@ class EndpointContext(OidcContext): + + self.setup_authentication() + +- self.session_manager = SessionManager( +- self.token_handler_args, +- sub_func=self._sub_func, +- conf=conf, +- upstream_get=self.unit_get) ++ # default is to have session management ++ if self.conf.get("session_management", self.conf["conf"].get("session_management", True)): ++ self.token_handler_args = get_token_handler_args(self.conf) ++ ++ self.session_manager = SessionManager( ++ self.token_handler_args, ++ sub_func=self._sub_func, ++ conf=conf, ++ upstream_get=self.unit_get) ++ else: ++ self.session_manager = None + + self.do_userinfo() + + # Must be done after userinfo + self.setup_login_hint_lookup() +- self.set_remember_token() ++ if self.session_manager: ++ self.set_remember_token() + + self.setup_client_authn_methods() + +- # _id_token_handler = self.session_manager.token_handler.handler.get("id_token") +- # if _id_token_handler: +- # self.provider_info.update(_id_token_handler.provider_info) ++ def get_metadata(self, supports: Optional[dict] = None, schema: Optional[Message] = None): ++ if supports is None: ++ supports = self.supports() ++ _metadata = self.claims.metadata(supports, schema) ++ _metadata.update(self._get_endpoint_info()) ++ return _metadata + + def setup_authz(self): + authz_spec = self.conf.get("authz") + +diff --git a/src/idpyoidc/server/session/grant.py b/src/idpyoidc/server/session/grant.py +index d7ee7c3..53d101c 100644 +--- a/src/idpyoidc/server/session/grant.py ++++ b/src/idpyoidc/server/session/grant.py +@@ -377,11 +377,10 @@ class Grant(Item): + ) + + logger.debug(f"token_payload: {token_payload}") +- + item.value = token_handler( + session_id=session_id, usage_rules=usage_rules, **token_payload + ) +- ++ logger.debug(f"token: {item.value}") + if based_on: + based_on.used += 1 + else: +diff --git a/src/idpyoidc/server/session/manager.py b/src/idpyoidc/server/session/manager.py +index ddd5a9d..bb38340 100644 +--- a/src/idpyoidc/server/session/manager.py ++++ b/src/idpyoidc/server/session/manager.py +@@ -1,10 +1,12 @@ + import hashlib + import logging + import os +-import uuid + from typing import Callable + from typing import List + from typing import Optional ++import uuid ++ ++from cryptojwt.jwe.fernet import FernetEncrypter + + from idpyoidc.encrypter import default_crypt_config + from idpyoidc.message.oauth2 import AuthorizationRequest +@@ -20,9 +22,9 @@ from .grant import Grant + from .grant import SessionToken + from .info import ClientSessionInfo + from .info import UserSessionInfo +-from ..token import handler + from ..token import UnknownToken + from ..token import WrongTokenClass ++from ..token import handler + from ..token.handler import TokenHandler + + logger = logging.getLogger(__name__) +@@ -84,6 +86,7 @@ def ephemeral_id(*args, **kwargs): + + class SessionManager(GrantManager): + parameter = Database.parameter.copy() ++ + # parameter.update({"salt": ""}) + init_args = ["token_handler_args", "upstream_get"] + +@@ -437,30 +440,6 @@ class SessionManager(GrantManager): + """ + self._revoke_tree(self.get_grant(session_id)) + +- # def grants( +- # self, +- # session_id: Optional[str] = "", +- # user_id: Optional[str] = "", +- # client_id: Optional[str] = "", +- # ) -> List[Grant]: +- # """ +- # Find all grant connected to a user session +- # +- # :param client_id: +- # :param user_id: +- # :param session_id: A session identifier +- # :return: A list of grants +- # """ +- # if session_id: +- # user_id, client_id, _ = self.decrypt_session_id(session_id) +- # elif user_id and client_id: +- # pass +- # else: +- # raise AttributeError("Must have session_id or user_id and client_id") +- # +- # _csi = self.get([user_id, client_id]) +- # return [self.get([user_id, client_id, gid]) for gid in _csi.subordinate] +- + def get_session_info( + self, + session_id: str, +@@ -551,5 +530,15 @@ class SessionManager(GrantManager): + def unpack_session_key(self, key): + return self.unpack_branch_key(key) + +-# def create_session_manager(upstream_get, token_handler_args, sub_func=None, conf=None): +-# return SessionManager(token_handler_args, sub_func=sub_func, conf=conf, upstream_get=upstream_get) ++ def get_client_id_from_token(self, token_value: str, handler_key: Optional[str] = ""): ++ if handler_key: ++ _token_info = self.token_handler.handler[handler_key].info(token_value) ++ else: ++ _token_info = self.token_handler.info(token_value) ++ ++ sid = _token_info.get("sid") ++ _path = self.decrypt_branch_id(sid) ++ if len(_path) == 3: ++ return _path[1] ++ else: ++ return _path[-1] +diff --git a/src/idpyoidc/server/token/__init__.py b/src/idpyoidc/server/token/__init__.py +index 8c92e56..d7a8a2a 100755 +--- a/src/idpyoidc/server/token/__init__.py ++++ b/src/idpyoidc/server/token/__init__.py +@@ -9,7 +9,6 @@ from idpyoidc.server.util import lv_pack + from idpyoidc.server.util import lv_unpack + from idpyoidc.time_util import utc_time_sans_frac + from idpyoidc.util import rndstr +- + from .exception import UnknownToken + from .exception import WrongTokenClass + +@@ -92,7 +91,7 @@ class DefaultToken(Token): + self.token_type = token_type + + def __call__( +- self, session_id: Optional[str] = "", token_class: Optional[str] = "", **payload ++ self, session_id: Optional[str] = "", token_class: Optional[str] = "", **payload + ) -> str: + """ + Return a token. +@@ -105,23 +104,40 @@ class DefaultToken(Token): + else: + token_class = "authorization_code" + ++ logger.debug(f"Mint {token_class}") ++ logger.debug(f"crypt.key: {self.crypt.key}") ++ _jwks = self.crypt_config.get('jwks', None) ++ logger.debug(f"crypt.jwks: {_jwks}") ++ + if self.lifetime >= 0: + exp = str(utc_time_sans_frac() + self.lifetime) + else: +- exp = "-1" # Live for ever ++ exp = "-1" # Live forever + + tmp = "" + rnd = "" + while rnd == tmp: # Don't use the same random value again + rnd = rndstr(32) # Ultimate length multiple of 16 + +- return base64.b64encode( ++ _args = { ++ "rnd": rnd, ++ "token_class": token_class, ++ "session_id": session_id, ++ "exp": exp ++ } ++ logger.debug(f"Encrypt arguments: {_args}") ++ _value = base64.urlsafe_b64encode( + self.crypt.encrypt(lv_pack(rnd, token_class, session_id, exp).encode()) + ).decode("utf-8") + ++ logger.debug(f"Token: {_value}") ++ return _value ++ + def split_token(self, token): ++ logger.debug(f"split_token: {token}") ++ logger.debug(f"crypt key: {self.crypt.key}") + try: +- plain = self.crypt.decrypt(base64.b64decode(token)) ++ plain = self.crypt.decrypt(base64.urlsafe_b64decode(token)) + except Exception as err: + raise UnknownToken(err) + # order: rnd, type, sid +diff --git a/src/idpyoidc/server/token/handler.py b/src/idpyoidc/server/token/handler.py +index 8fa9063..6289812 100755 +--- a/src/idpyoidc/server/token/handler.py ++++ b/src/idpyoidc/server/token/handler.py +@@ -137,6 +137,8 @@ def default_token(spec): + else: + return False + ++def key_types(keys): ++ return [k["kid"] for k in keys] + + JWKS_FILE = "private/token_jwks.json" + +@@ -192,10 +194,18 @@ def factory( + ("token", token, "access_token"), + ("refresh", refresh, "refresh_token"), + ]: +- if cnf is not None: +- if default_token(cnf): +- if kj: +- _add_passwd(kj, cnf, cls) ++ if cnf is not None: # else just default ++ try: ++ _key_types = key_types(cnf["kwargs"]["crypt_conf"]["kwargs"]["keys"]["key_defs"]) ++ except KeyError: # will fail on keys if it fails ++ pass ++ else: ++ if "key" in _key_types and "password" in _key_types: ++ raise ValueError("You have to chose one of key or password") ++ if "password" not in _key_types and "key" not in _key_types: ++ if kj: ++ _add_passwd(kj, cnf, cls) ++ logger.debug(f"init_token_handler: {cls}") + args[attr] = init_token_handler(upstream_get, cnf, token_class_map[cls]) + + if id_token is not None: +diff --git a/src/idpyoidc/server/token/jwt_token.py b/src/idpyoidc/server/token/jwt_token.py +index abbfa97..534bb49 100644 +--- a/src/idpyoidc/server/token/jwt_token.py ++++ b/src/idpyoidc/server/token/jwt_token.py +@@ -1,3 +1,4 @@ ++import logging + from typing import Callable + from typing import Optional + from typing import Union +@@ -7,30 +8,32 @@ from cryptojwt.jws.exception import JWSException + from cryptojwt.utils import importer + + from idpyoidc.server.exception import ToOld +- +-from ...message import Message +-from ...message.oauth2 import JWTAccessToken +-from ..constant import DEFAULT_TOKEN_LIFETIME +-from . import Token + from . import is_expired ++from . import Token + from .exception import UnknownToken + from .exception import WrongTokenClass ++from ..constant import DEFAULT_TOKEN_LIFETIME ++from ...message import Message ++from ...message.oauth2 import JWTAccessToken ++ ++logger = logging.getLogger(__name__) + + + class JWTToken(Token): ++ + def __init__( +- self, +- token_class, +- # keyjar: KeyJar = None, +- issuer: str = None, +- aud: Optional[list] = None, +- alg: str = "ES256", +- lifetime: int = DEFAULT_TOKEN_LIFETIME, +- upstream_get: Callable = None, +- token_type: str = "Bearer", +- profile: Optional[Union[Message, str]] = JWTAccessToken, +- with_jti: Optional[bool] = False, +- **kwargs ++ self, ++ token_class, ++ # keyjar: KeyJar = None, ++ issuer: str = None, ++ aud: Optional[list] = None, ++ alg: str = "ES256", ++ lifetime: int = DEFAULT_TOKEN_LIFETIME, ++ upstream_get: Callable = None, ++ token_type: str = "Bearer", ++ profile: Optional[Union[Message, str]] = JWTAccessToken, ++ with_jti: Optional[bool] = False, ++ **kwargs + ): + Token.__init__(self, token_class, **kwargs) + self.token_type = token_type +@@ -59,13 +62,13 @@ class JWTToken(Token): + return payload + + def __call__( +- self, +- session_id: Optional[str] = "", +- token_class: Optional[str] = "", +- usage_rules: Optional[dict] = None, +- profile: Optional[Message] = None, +- with_jti: Optional[bool] = None, +- **payload ++ self, ++ session_id: Optional[str] = "", ++ token_class: Optional[str] = "", ++ usage_rules: Optional[dict] = None, ++ profile: Optional[Message] = None, ++ with_jti: Optional[bool] = None, ++ **payload + ) -> str: + """ + Return a token. +@@ -89,8 +92,10 @@ class JWTToken(Token): + lifetime = usage_rules.get("expires_in") + else: + lifetime = self.lifetime ++ _keyjar = self.upstream_get("attribute", "keyjar") ++ logger.info(f"Key owners in the keyjar: {_keyjar.owners()}") + signer = JWT( +- key_jar=self.upstream_get("attribute", "keyjar"), ++ key_jar=_keyjar, + iss=self.issuer, + lifetime=lifetime, + sign_alg=self.alg, +diff --git a/src/idpyoidc/server/xx_metadata.py b/src/idpyoidc/server/xx_metadata.py +new file mode 100644 +index 0000000..a75f852 +--- /dev/null ++++ b/src/idpyoidc/server/xx_metadata.py +@@ -0,0 +1,54 @@ ++from typing import Callable ++from typing import List ++from typing import Optional ++ ++from idpyoidc.message import Message ++ ++from idpyoidc.transform import preferred_to_registered ++ ++ ++class XMetadata(): ++ def __int__(self, upstream_get: Callable): ++ self.upstream_get = upstream_get ++ ++ def get_endpoint_claims(self, entity): ++ _info = {} ++ for endp in entity.server.endpoint.values(): ++ if endp.endpoint_name: ++ _info[endp.endpoint_name] = endp.full_path ++ for arg, claim in [("client_authn_method", "auth_methods"), ++ ("auth_signing_alg_values", "auth_signing_alg_values")]: ++ _val = getattr(endp, arg, None) ++ if _val: ++ # trust_mark_status_endpoint_auth_methods_supported ++ md_param = f"{endp.endpoint_name}_{claim}" ++ _info[md_param] = _val ++ return _info ++ ++ def __call__(self, ++ entity_type: str, ++ metadata_schema: Optional[Message] = None, ++ extra_claims: Optional[List[str]] = None, ++ **kwargs): ++ _claims = self.upstream_get("context").claims ++ entity = self.upstream_get("unit") ++ if not _claims.use: ++ _claims.use = preferred_to_registered(_claims.prefer, supported=entity.supports()) ++ ++ metadata = _claims.use ++ # the claims that can appear in the metadata ++ if metadata_schema: ++ attr = list(metadata_schema.c_param.keys()) ++ else: ++ attr = [] ++ ++ if extra_claims: ++ attr.extend(extra_claims) ++ ++ if attr: ++ metadata = {k:v for k,v in metadata.items() if k in attr} ++ ++ # collect endpoints ++ metadata.update(self.get_endpoint_claims(entity)) ++ # _issuer = getattr(self.server.context, "trust_mark_server", None) ++ return {entity_type: metadata} +diff --git a/src/idpyoidc/storage/abfile.py b/src/idpyoidc/storage/abfile.py +index 6257fe2..d1c088f 100644 +--- a/src/idpyoidc/storage/abfile.py ++++ b/src/idpyoidc/storage/abfile.py +@@ -191,7 +191,7 @@ class AbstractFileSystem(DictType): + else: + return False + else: +- logger.error("Could not access {}".format(fname)) ++ logger.error(f"Not a file '{fname}'") + raise KeyError(item) + + def _read_info(self, fname): +@@ -239,6 +239,14 @@ class AbstractFileSystem(DictType): + else: + self.fmtime[f] = mtime + ++ _keys = self.storage.keys() ++ for f in _keys: ++ fname = os.path.join(self.fdir, f) ++ if os.path.isfile(fname): ++ pass ++ else: ++ del self.storage[f] ++ + def items(self): + """ + Implements the dict.items() method +diff --git a/src/idpyoidc/storage/abfile_no_cache.py b/src/idpyoidc/storage/abfile_no_cache.py +new file mode 100644 +index 0000000..114d45d +--- /dev/null ++++ b/src/idpyoidc/storage/abfile_no_cache.py +@@ -0,0 +1,211 @@ ++import logging ++import os ++import time ++from typing import Optional ++ ++from cryptojwt.utils import importer ++from filelock import FileLock ++ ++from idpyoidc.storage import DictType ++from idpyoidc.util import PassThru ++from idpyoidc.util import QPKey ++ ++logger = logging.getLogger(__name__) ++ ++ ++class AbstractFileSystemNoCache(DictType): ++ """ ++ FileSystem implements a simple file based database. ++ It has a dictionary like interface. ++ Each key maps one-to-one to a file on disc, where the content of the ++ file is the value. ++ ONLY goes one level deep. ++ Not directories in directories. ++ """ ++ ++ def __init__( ++ self, ++ fdir: Optional[str] = "", ++ key_conv: Optional[str] = "", ++ value_conv: Optional[str] = "", ++ read_only: Optional[bool] = False, ++ **kwargs ++ ): ++ """ ++ items = FileSystem( ++ { ++ 'fdir': fdir, ++ 'key_conv':{'to': quote_plus, 'from': unquote_plus}, ++ 'value_conv':{'to': keyjar_to_jwks, 'from': jwks_to_keyjar} ++ }) ++ ++ :param fdir: The root of the directory ++ :param key_conv: Converts to/from the key displayed by this class to ++ users of it to something that can be used as a file name. ++ The value of key_conv is a class that has the methods 'serialize'/'deserialize'. ++ :param value_conv: As with key_conv you can convert/translate ++ the value bound to a key in the database to something that can easily ++ be stored in a file. Like with key_conv the value of this parameter ++ is a class that has the methods 'serialize'/'deserialize'. ++ """ ++ super(AbstractFileSystemNoCache, self).__init__( ++ fdir=fdir, key_conv=key_conv, value_conv=value_conv ++ ) ++ ++ self.fdir = fdir ++ self.read_only = read_only ++ ++ if key_conv: ++ self.key_conv = importer(key_conv)() ++ else: ++ self.key_conv = QPKey() ++ ++ if value_conv: ++ self.value_conv = importer(value_conv)() ++ else: ++ self.value_conv = PassThru() ++ ++ if not os.path.isdir(self.fdir): ++ os.makedirs(self.fdir) ++ ++ def get(self, item, default=None): ++ try: ++ return self[item] ++ except KeyError: ++ return default ++ ++ def __getitem__(self, item): ++ """ ++ Return the value bound to an identifier. ++ ++ :param item: The identifier. ++ :return: ++ """ ++ _file_name = self.key_conv.serialize(item) ++ logger.debug(f'Read from "{_file_name}"') ++ return self._read_info(_file_name) ++ ++ def __setitem__(self, key, value): ++ """ ++ Binds a value to a specific key. If the file that the key maps to ++ does not exist it will be created. The content of the file will be ++ set to the value given. ++ ++ :param key: Identifier ++ :param value: Value that should be bound to the identifier. ++ :return: ++ """ ++ ++ if self.read_only: ++ return ++ ++ if not os.path.isdir(self.fdir): ++ os.makedirs(self.fdir, exist_ok=True) ++ ++ try: ++ _file_name = self.key_conv.serialize(key) ++ except KeyError: ++ _file_name = key ++ ++ fname = os.path.join(self.fdir, _file_name) ++ lock = FileLock(f"{fname}.lock") ++ with lock: ++ with open(fname, "w") as fp: ++ fp.write(self.value_conv.serialize(value)) ++ ++ logger.debug(f'Wrote to "{_file_name}"') ++ ++ def __delitem__(self, key): ++ if self.read_only: ++ return ++ ++ fname = os.path.join(self.fdir, key) ++ if fname.endswith(".lock"): ++ if os.path.isfile(fname): ++ os.unlink(fname) ++ else: ++ if os.path.isfile(fname): ++ lock = FileLock(f"{fname}.lock") ++ with lock: ++ os.unlink(fname) ++ os.unlink(f"{fname}.lock") ++ ++ def _keys(self): ++ """ ++ Implements the dict.keys() method ++ """ ++ keys = [] ++ for f in os.listdir(self.fdir): ++ fname = os.path.join(self.fdir, f) ++ ++ if not os.path.isfile(fname): ++ continue ++ if fname.endswith(".lock"): ++ continue ++ ++ keys.append(f) ++ ++ return keys ++ ++ def keys(self): ++ return [self.key_conv.deserialize(k) for k in self._keys()] ++ ++ def _read_info(self, key): ++ file_name = os.path.join(self.fdir, key) ++ if os.path.isfile(file_name): ++ try: ++ lock = FileLock(f"{file_name}.lock") ++ with lock: ++ info = open(file_name, "r").read().strip() ++ lock.release() ++ return self.value_conv.deserialize(info) ++ except Exception as err: ++ logger.error(err) ++ raise ++ else: ++ _msg = f"No such file: '{file_name}'" ++ logger.error(_msg) ++ return None ++ ++ def items(self): ++ """ ++ Implements the dict.items() method ++ """ ++ for k in self._keys(): ++ v = self._read_info(k) ++ yield self.key_conv.deserialize(k), v ++ ++ def clear(self): ++ """ ++ Completely resets the database. This means that all information in ++ the local cache and on disc will be erased. ++ """ ++ if self.read_only: ++ return ++ ++ if not os.path.isdir(self.fdir): ++ os.makedirs(self.fdir, exist_ok=True) ++ return ++ ++ for f in os.listdir(self.fdir): ++ del self[f] ++ ++ def __contains__(self, item): ++ file_name = os.path.join(self.fdir, self.key_conv.serialize(item)) ++ if os.path.isfile(file_name): ++ return True ++ else: ++ return False ++ ++ def __iter__(self): ++ for k in self._keys(): ++ yield self.key_conv.deserialize(k) ++ ++ def __call__(self, *args, **kwargs): ++ return [self.key_conv.deserialize(k) for k in self._keys()] ++ ++ def __len__(self): ++ if not os.path.isdir(self.fdir): ++ return 0 ++ ++ return len(self._keys()) +diff --git a/src/idpyoidc/storage/listfile.py b/src/idpyoidc/storage/listfile.py +index 77520de..fb515e3 100644 +--- a/src/idpyoidc/storage/listfile.py ++++ b/src/idpyoidc/storage/listfile.py +@@ -111,6 +111,49 @@ class ReadOnlyListFile(object): + else: + return None + ++ def __len__(self): ++ _lst = self._read_info(self.file_name) ++ if _lst is None or _lst == []: ++ return 0 ++ ++ return len(set(_lst)) ++ ++ def _read_info(self, fname): ++ if os.path.isfile(fname): ++ try: ++ lock = FileLock(f"{fname}.lock") ++ with lock: ++ fp = open(fname, "r") ++ info = [x.strip() for x in fp.readlines()] ++ lock.release() ++ return list(set(info)) ++ except Exception as err: ++ logger.error(err) ++ raise ++ else: ++ _msg = f"No such file: '{fname}'" ++ logger.error(_msg) ++ return None ++ ++ def __call__(self): ++ return self._read_info(self.file_name) ++ ++ def list(self): ++ return self._read_info(self.file_name) ++ ++class ReadWriteListFile(object): ++ ++ def __init__(self, file_name): ++ self.file_name = file_name ++ ++ if not os.path.exists(file_name): ++ fp = open(file_name, "x") ++ fp.close() ++ ++ def __contains__(self, item): ++ _lst = self._read_info(self.file_name) ++ return item in _lst ++ + def __len__(self): + _lst = self._read_info(self.file_name) + if _lst is None or _lst == []: +diff --git a/src/idpyoidc/client/claims/transform.py b/src/idpyoidc/transform.py +similarity index 94% +rename from src/idpyoidc/client/claims/transform.py +rename to src/idpyoidc/transform.py +index 1ca40c6..3834006 100644 +--- a/src/idpyoidc/client/claims/transform.py ++++ b/src/idpyoidc/transform.py +@@ -51,10 +51,10 @@ REQUEST2REGISTER = { + + + def supported_to_preferred( +- supported: dict, +- preference: dict, +- base_url: str, +- info: Optional[dict] = None, ++ supported: dict, ++ preference: dict, ++ base_url: str, ++ info: Optional[dict] = None, + ): + if info: # The provider info + for key, val in supported.items(): +@@ -83,7 +83,7 @@ def supported_to_preferred( + preference[key] = [x for x in val if x in _info_val] + else: + pass +- else: ++ elif val: + preference[key] = val + + # special case -> must have a request_uris value +@@ -148,7 +148,7 @@ def _intersection(a, b): + + + def preferred_to_registered( +- prefers: dict, supported: dict, registration_response: Optional[dict] = None ++ prefers: dict, supported: dict, registration_response: Optional[dict] = None + ): + """ + The claims with values that are returned from the OP is what goes unless (!!) +@@ -200,7 +200,7 @@ def preferred_to_registered( + # be a singleton or an array. So just add it as is. + registered[_reg_key] = val + +- logger.debug(f"Entity registered: {registered}") ++ logger.debug(f"preferred2registered: {registered}") + return registered + + +@@ -219,4 +219,10 @@ def create_registration_request(prefers: dict, supported: dict) -> dict: + continue + + _request[key] = array_or_singleton(spec, value) ++ ++ for key, val in prefers.items(): ++ if key not in RegistrationRequest.c_param.keys(): ++ if key not in REGISTER2PREFERRED.values(): ++ _request[key] = val ++ + return _request diff --git a/patch/metadata.patch b/patch/metadata.patch new file mode 100644 index 00000000..1730fe54 --- /dev/null +++ b/patch/metadata.patch @@ -0,0 +1,63 @@ +diff --git a/src/idpyoidc/message/__init__.py b/src/idpyoidc/message/__init__.py +index 46d2344..df3e5b3 100644 +--- a/src/idpyoidc/message/__init__.py ++++ b/src/idpyoidc/message/__init__.py +@@ -83,7 +83,8 @@ class Message(MutableMapping): + """ + Creates a string using the application/x-www-form-urlencoded format + +- :doseq: If set to true, key=value pairs separated by '&' are generated for each element of the value sequence for the key. ++ :doseq: If set to true, key=value pairs separated by '&' are generated for each element ++ of the value sequence for the key. + :return: A string of the application/x-www-form-urlencoded format + """ + +@@ -388,7 +389,7 @@ class Message(MutableMapping): + else: + self._dict[skey] = val + else: +- raise DecodeError(ERRTXT % (key, "type != %s" % vtype)) ++ raise DecodeError(ERRTXT % (key, f"type != {vtype}, val:{val}, type:{type(val)}")) + else: + if val is None: + self._dict[skey] = None + +diff --git a/src/idpyoidc/message/oauth2/__init__.py b/src/idpyoidc/message/oauth2/__init__.py +index 788fe8c..95440a9 100644 +--- a/src/idpyoidc/message/oauth2/__init__.py ++++ b/src/idpyoidc/message/oauth2/__init__.py +@@ -560,6 +560,12 @@ class PushedAuthorizationRequest(AuthorizationRequest): + return True + + ++class PushedAuthorizationResponse(ResponseMessage): ++ c_param = ResponseMessage.c_param.copy() ++ c_param.update({"request_uri": SINGLE_REQUIRED_STRING}) ++ ++ ++ + class SecurityEventToken(Message): + c_param = { + "iss": SINGLE_REQUIRED_STRING, + +diff --git a/src/idpyoidc/message/oidc/__init__.py b/src/idpyoidc/message/oidc/__init__.py +index d266224..4cef07a 100644 +--- a/src/idpyoidc/message/oidc/__init__.py ++++ b/src/idpyoidc/message/oidc/__init__.py +@@ -1025,7 +1025,7 @@ class JsonWebToken(Message): + except KeyError: + pass + +- if "iss" in kwargs and "iss" in self: ++ if "iss" in kwargs and kwargs["iss"] and "iss" in self: + if kwargs["iss"] != self["iss"]: + raise ValueError("Wrong issuer") + +@@ -1191,7 +1191,7 @@ def make_openid_request( + :param request_object_signing_alg: Which signing algorithm to use + :param recv: The intended receiver of the request + :param with_jti: Whether a JTI should be included in the JWT. +- :param lifetime: How long the JWT is expect to be live. ++ :param lifetime: How long the JWT is expected to be alive. + :return: JWT encoded OpenID request + """ diff --git a/patch/oauth2.patch b/patch/oauth2.patch new file mode 100644 index 00000000..444f10a7 --- /dev/null +++ b/patch/oauth2.patch @@ -0,0 +1,954 @@ + +diff --git a/src/idpyoidc/client/client_auth.py b/src/idpyoidc/client/client_auth.py +index a8830cd..baf03d9 100755 +--- a/src/idpyoidc/client/client_auth.py ++++ b/src/idpyoidc/client/client_auth.py +@@ -10,6 +10,7 @@ from cryptojwt.jws.jws import SIGNER_ALGS + from cryptojwt.jws.utils import alg2keytype + from cryptojwt.utils import importer + ++from idpyoidc.client.request_object import construct_request_parameter + from idpyoidc.defaults import DEF_SIGN_ALG + from idpyoidc.defaults import JWT_BEARER + from idpyoidc.message import Message +@@ -31,6 +32,7 @@ __author__ = "roland hedberg" + + DEFAULT_ACCESS_TOKEN_TYPE = "Bearer" + ++ + class AuthnFailure(Exception): + """Unspecified Authentication failure""" + +@@ -46,7 +48,7 @@ def assertion_jwt(client_id, keys, audience, algorithm, lifetime=600): + + :param client_id: The Client ID + :param keys: Signing keys +- :param audience: Who is the receivers for this assertion ++ :param audience: Who's the receivers for this assertion + :param algorithm: Signing algorithm + :param lifetime: The lifetime of the signed Json Web Token + :return: A Signed Json Web Token +@@ -628,6 +630,12 @@ class PrivateKeyJWT(JWSAuthnMethod): + return keyjar.get_signing_key(alg2keytype(algorithm), "", alg=algorithm) + + ++class RequestParam(ClientAuthnMethod): ++ def construct(self, request, service=None, http_args=None, **kwargs): ++ request_object = construct_request_parameter(service, request, **kwargs) ++ request["request"] = request_object ++ ++ + # Map from client authentication identifiers to corresponding class + CLIENT_AUTHN_METHOD = { + "client_secret_basic": ClientSecretBasic, +@@ -637,6 +645,7 @@ CLIENT_AUTHN_METHOD = { + "client_secret_jwt": ClientSecretJWT, + "private_key_jwt": PrivateKeyJWT, + # "client_notification_authn": ClientNotificationAuthn ++ "request_param": RequestParam + } + + TYPE_METHOD = [(JWT_BEARER, JWSAuthnMethod)] + + +diff --git a/src/idpyoidc/client/entity.py b/src/idpyoidc/client/entity.py +index 197d5d7..2a1b0a6 100644 +--- a/src/idpyoidc/client/entity.py ++++ b/src/idpyoidc/client/entity.py +@@ -103,8 +103,9 @@ class Entity(Unit): # This is a Client. What type is undefined here. + if config is None: + config = {} + ++ # Client ID is set through configuration or at registration + _id = config.get("client_id") +- self.client_id = self.entity_id = entity_id or config.get("entity_id", _id) ++ self.entity_id = entity_id or config.get("entity_id", _id) + + Unit.__init__( + self, +@@ -114,7 +115,7 @@ class Entity(Unit): # This is a Client. What type is undefined here. + httpc_params=httpc_params, + config=config, + key_conf=key_conf, +- client_id=self.client_id, ++ client_id=_id, + ) + + if services: + +diff --git a/src/idpyoidc/client/oauth2/__init__.py b/src/idpyoidc/client/oauth2/__init__.py +index 620608b..20c5c13 100755 +--- a/src/idpyoidc/client/oauth2/__init__.py ++++ b/src/idpyoidc/client/oauth2/__init__.py +@@ -14,6 +14,7 @@ from idpyoidc.client.service import REQUEST_INFO + from idpyoidc.client.service import SUCCESSFUL + from idpyoidc.client.service import Service + from idpyoidc.client.util import do_add_ons ++from idpyoidc.client.util import get_content_type + from idpyoidc.client.util import get_deserialization_method + from idpyoidc.configure import Configuration + from idpyoidc.context import OidcContext +@@ -254,12 +255,13 @@ class Client(Entity): + + if reqresp.status_code in SUCCESSFUL: + logger.debug('response_body_type: "{}"'.format(response_body_type)) +- _deser_method = get_deserialization_method(reqresp) ++ _content_type = get_content_type(reqresp) ++ _deser_method = get_deserialization_method(_content_type) + +- if _deser_method != response_body_type: ++ if _content_type != response_body_type: + logger.warning( + "Not the body type I expected: {} != {}".format( +- _deser_method, response_body_type ++ _content_type, response_body_type + ) + ) + if _deser_method in ["json", "jwt", "urlencoded"]: +@@ -282,7 +284,9 @@ class Client(Entity): + elif 400 <= reqresp.status_code < 500: + logger.error("Error response ({}): {}".format(reqresp.status_code, reqresp.text)) + # expecting an error response +- _deser_method = get_deserialization_method(reqresp) ++ _content_type = get_content_type(reqresp) ++ _deser_method = get_deserialization_method(_content_type) ++ + if not _deser_method: + _deser_method = "json" + + diff --git a/src/idpyoidc/client/oauth2/add_on/dpop.py b/src/idpyoidc/client/oauth2/add_on/dpop.py +index b845050..edd4a0e 100644 +--- a/src/idpyoidc/client/oauth2/add_on/dpop.py ++++ b/src/idpyoidc/client/oauth2/add_on/dpop.py +@@ -1,8 +1,10 @@ ++import base64 + import logging + import uuid + from hashlib import sha256 + from typing import Optional + ++from cryptojwt import as_unicode + from cryptojwt.jwk.jwk import key_from_jwk_dict + from cryptojwt.jws.jws import factory + from cryptojwt.jws.jws import JWS +@@ -16,7 +18,7 @@ from idpyoidc.message import SINGLE_OPTIONAL_STRING + from idpyoidc.message import SINGLE_REQUIRED_INT + from idpyoidc.message import SINGLE_REQUIRED_JSON + from idpyoidc.message import SINGLE_REQUIRED_STRING +-from idpyoidc.metadata import get_signing_algs ++from idpyoidc.alg_info import get_signing_algs + from idpyoidc.time_util import utc_time_sans_frac + + logger = logging.getLogger(__name__) +@@ -149,7 +151,7 @@ def dpop_header( + } + + if token: +- header_dict["ath"] = sha256(token.encode("utf8")).hexdigest() ++ header_dict["ath"] = as_unicode(base64.urlsafe_b64encode(sha256(token.encode("utf8")).digest())) + + if nonce: + header_dict["nonce"] = nonce +@@ -168,14 +170,18 @@ def dpop_header( + + def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=None): + """ +- Add the necessary pieces to make pushed authorization happen. ++ Add the necessary pieces to make DPoP happen. + + :param services: A dictionary with all the services the client has access to. +- :param signing_algorithms: Allowed signing algorithms, there is no default algorithms ++ :param dpop_signing_alg_values_supported: Allowed signing algorithms, there is no default algorithms ++ :param with_dpop_header: Which services that should add a DPoP header to a request + """ + +- # Access token request should use DPoP header +- _service = services["accesstoken"] ++ if with_dpop_header is None: ++ with_dpop_header = ["accesstoken", "userinfo"] ++ ++ _service = services[with_dpop_header[0]] ++ # Add to Context + _context = _service.upstream_get("context") + _algs_supported = [ + alg for alg in dpop_signing_alg_values_supported if alg in get_signing_algs() +@@ -186,20 +192,8 @@ def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=No + } + _context.set_preference("dpop_signing_alg_values_supported", _algs_supported) + +- _service.construct_extra_headers.append(dpop_header) +- +- # The same for userinfo requests +- _userinfo_service = services.get("userinfo") +- if _userinfo_service: +- _userinfo_service.construct_extra_headers.append(dpop_header) +- # To be backward compatible +- if with_dpop_header is None: +- with_dpop_header = ["userinfo"] +- +- # Add dpop HTTP header to these ++ # Add dpop HTTP header to requests by these services + for _srv in with_dpop_header: +- if _srv == "accesstoken": +- continue + _service = services.get(_srv) + if _service: + _service.construct_extra_headers.append(dpop_header) + +diff --git a/src/idpyoidc/client/oauth2/add_on/jar.py b/src/idpyoidc/client/oauth2/add_on/jar.py +index a775532..5c98c76 100644 +--- a/src/idpyoidc/client/oauth2/add_on/jar.py ++++ b/src/idpyoidc/client/oauth2/add_on/jar.py +@@ -1,12 +1,9 @@ + import logging + from typing import Optional + +-from idpyoidc import claims +-from idpyoidc import metadata +-from idpyoidc.client.oidc.utils import construct_request_uri +-from idpyoidc.client.oidc.utils import request_object_encryption +-from idpyoidc.message.oidc import make_openid_request +-from idpyoidc.time_util import utc_time_sans_frac ++from idpyoidc import alg_info ++from idpyoidc.client.request_object import construct_request_parameter ++from idpyoidc.client.request_object import construct_request_uri + + logger = logging.getLogger(__name__) + +@@ -35,94 +32,6 @@ def store_request_on_file(service, req, **kwargs): + return _webname + + +-def get_request_object_signing_alg(service, **kwargs): +- alg = "" +- for arg in ["request_object_signing_alg", "algorithm"]: +- try: # Trumps everything +- alg = kwargs[arg] +- except KeyError: +- pass +- else: +- break +- +- if not alg: +- _context = service.upstream_get("context") +- alg = _context.add_on["jar"].get("request_object_signing_alg") +- if alg is None: +- alg = "RS256" +- return alg +- +- +-def construct_request_parameter(service, req, audience=None, **kwargs): +- """Construct a request parameter""" +- alg = get_request_object_signing_alg(service, **kwargs) +- kwargs["request_object_signing_alg"] = alg +- +- _context = service.upstream_get("context") +- if "keys" not in kwargs and alg and alg != "none": +- kwargs["keys"] = service.upstream_get("attribute", "keyjar") +- +- if alg == "none": +- kwargs["keys"] = [] +- +- # This is the issuer of the JWT, that is me ! +- _issuer = kwargs.get("issuer") +- if _issuer is None: +- kwargs["issuer"] = _context.get_client_id() +- +- if kwargs.get("recv") is None: +- try: +- kwargs["recv"] = _context.provider_info["issuer"] +- except KeyError: +- kwargs["recv"] = _context.issuer +- +- try: +- del kwargs["service"] +- except KeyError: +- pass +- +- _jar_conf = _context.add_on["jar"] +- expires_in = _jar_conf.get("expires_in", DEFAULT_EXPIRES_IN) +- if expires_in: +- req["exp"] = utc_time_sans_frac() + int(expires_in) +- +- if _jar_conf.get("with_jti", False): +- kwargs["with_jti"] = True +- +- _enc_enc = _jar_conf.get("request_object_encryption_enc", "") +- if _enc_enc: +- kwargs["request_object_encryption_enc"] = _enc_enc +- kwargs["request_object_encryption_alg"] = _jar_conf.get("request_object_encryption_alg") +- +- # Filter out only the arguments I want +- _mor_args = { +- k: kwargs[k] +- for k in [ +- "keys", +- "issuer", +- "request_object_signing_alg", +- "recv", +- "with_jti", +- "lifetime", +- ] +- if k in kwargs +- } +- +- if audience: +- _mor_args["aud"] = audience +- +- _req_jwt = make_openid_request(req, **_mor_args) +- +- if "target" not in kwargs: +- kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) +- +- # Should the request be encrypted +- _req_jwte = request_object_encryption( +- _req_jwt, _context, service.upstream_get("attribute", "keyjar"), **kwargs +- ) +- return _req_jwte +- +- + def jar_post_construct(request_args, service, **kwargs): + """ + Modify the request arguments. +@@ -175,14 +84,14 @@ def jar_post_construct(request_args, service, **kwargs): + + + def add_support( +- service, +- request_type: Optional[str] = "request_parameter", +- request_dir: Optional[str] = "", +- request_object_signing_alg: Optional[str] = "RS256", +- expires_in: Optional[int] = DEFAULT_EXPIRES_IN, +- with_jti: Optional[bool] = False, +- request_object_encryption_alg: Optional[str] = "", +- request_object_encryption_enc: Optional[str] = "", ++ service, ++ request_type: Optional[str] = "request_parameter", ++ request_dir: Optional[str] = "", ++ request_object_signing_alg: Optional[str] = "RS256", ++ expires_in: Optional[int] = DEFAULT_EXPIRES_IN, ++ with_jti: Optional[bool] = False, ++ request_object_encryption_alg: Optional[str] = "", ++ request_object_encryption_enc: Optional[str] = "", + ): + """ + JAR support can only be considered if this client can access an authorization service. +@@ -208,8 +117,8 @@ def add_support( + args["request_dir"] = request_dir + + if request_object_encryption_enc and request_object_encryption_alg: +- if request_object_encryption_enc in metadata.get_encryption_encs(): +- if request_object_encryption_alg in metadata.get_encryption_algs(): ++ if request_object_encryption_enc in alg_info.get_encryption_encs(): ++ if request_object_encryption_alg in alg_info.get_encryption_algs(): + args["request_object_encryption_enc"] = request_object_encryption_enc + args["request_object_encryption_alg"] = request_object_encryption_alg + else: +diff --git a/src/idpyoidc/client/oauth2/add_on/par.py b/src/idpyoidc/client/oauth2/add_on/par.py +index afa9405..cfdc349 100644 +--- a/src/idpyoidc/client/oauth2/add_on/par.py ++++ b/src/idpyoidc/client/oauth2/add_on/par.py +@@ -1,8 +1,11 @@ + import logging + ++from cryptojwt import JWT + from cryptojwt.utils import importer + + from idpyoidc.client.client_auth import CLIENT_AUTHN_METHOD ++from idpyoidc.client.oauth2.utils import set_request_object ++from idpyoidc.client.service import Service + from idpyoidc.message import Message + from idpyoidc.message.oauth2 import JWTSecuredAuthorizationRequest + from idpyoidc.server.util import execute +@@ -13,7 +16,7 @@ logger = logging.getLogger(__name__) + HTTP_METHOD = "POST" + + +-def push_authorization(request_args, service, **kwargs): ++def push_authorization(request_args: Message, service: Service, **kwargs): + """ + :param request_args: All the request arguments as a AuthorizationRequest instance + :param service: The service to which this post construct method is applied. +@@ -48,19 +51,31 @@ def push_authorization(request_args, service, **kwargs): + ) + _headers["Content-Type"] = "application/x-www-form-urlencoded" + +- # construct the message body +- _body = request_args.to_urlencoded() ++ if isinstance(request_args, Message): ++ _required_params = request_args.to_dict() ++ else: ++ _required_params = request_args ++ ++ _add_request_object = kwargs.get("add_request_object", False) ++ if _add_request_object: ++ _required_params["request"] = set_request_object(service, request_args) ++ ++ _req = service.msg_type(**_required_params) ++ _body = _req.to_urlencoded() + + _http_client = method_args.get("http_client", None) + if not _http_client: + _http_client = service.upstream_get("unit").httpc + + _httpc_params = service.upstream_get("unit").httpc_params ++ _par_endpoint = kwargs.get("pushed_authorization_request_endpoint", None) ++ if not _par_endpoint: ++ _par_endpoint = _context.provider_info["pushed_authorization_request_endpoint"] + + # Send it to the Pushed Authorization Request Endpoint using POST + resp = _http_client( + method=HTTP_METHOD, +- url=_context.provider_info["pushed_authorization_request_endpoint"], ++ url=_par_endpoint, + data=_body, + headers=_headers, + **_httpc_params +@@ -73,10 +88,7 @@ def push_authorization(request_args, service, **kwargs): + _req[param] = request_args.get(param) + request_args = _req + else: +- raise ConnectionError( +- f"Could not connect to " +- f'{_context.provider_info["pushed_authorization_request_endpoint"]}' +- ) ++ raise ConnectionError(f"Could not connect to {_par_endpoint}") + + return request_args + + diff --git a/src/idpyoidc/client/oauth2/authorization.py b/src/idpyoidc/client/oauth2/authorization.py +index 9d85f1f..04ae98d 100644 +--- a/src/idpyoidc/client/oauth2/authorization.py ++++ b/src/idpyoidc/client/oauth2/authorization.py +@@ -31,7 +31,7 @@ class Authorization(Service): + + _supports = { + "response_types_supported": ["code"], +- "response_modes_supported": ["query", "fragment"], ++ "grant_types": None + } + + _callback_path = { + +diff --git a/src/idpyoidc/client/oauth2/pushed_authorization.py b/src/idpyoidc/client/oauth2/pushed_authorization.py +new file mode 100644 +index 0000000..20eb299 +--- /dev/null ++++ b/src/idpyoidc/client/oauth2/pushed_authorization.py +@@ -0,0 +1,89 @@ ++"""The service that talks to the OAuth2 Authorization endpoint.""" ++import logging ++ ++from idpyoidc.client.oauth2.utils import get_state_parameter ++from idpyoidc.client.oauth2.utils import pre_construct_pick_redirect_uri ++from idpyoidc.client.oauth2.utils import set_request_object ++from idpyoidc.client.oauth2.utils import set_state_parameter ++from idpyoidc.client.service import Service ++from idpyoidc.exception import MissingParameter ++from idpyoidc.message import oauth2 ++from idpyoidc.message.oauth2 import ResponseMessage ++from idpyoidc.time_util import time_sans_frac ++ ++LOGGER = logging.getLogger(__name__) ++ ++ ++class PushedAuthorization(Service): ++ """The service that talks to the OAuth2 Pushed Authorization endpoint.""" ++ ++ msg_type = oauth2.PushedAuthorizationRequest ++ response_cls = oauth2.PushedAuthorizationResponse ++ error_msg = ResponseMessage ++ endpoint_name = "pushed_authorization_request_endpoint" ++ service_name = "pushed_authorization" ++ response_body_type = "json" ++ http_method = "POST" ++ ++ _supports = { ++ "response_types_supported": ["code"], ++ "grant_types": None ++ } ++ ++ def __init__(self, upstream_get, conf=None): ++ Service.__init__(self, upstream_get, conf=conf) ++ self.pre_construct.extend([pre_construct_pick_redirect_uri, set_state_parameter]) ++ self.post_construct.append(self.store_auth_request) ++ ++ def add_(self, request_args=None, **kwargs): ++ _add_request_object = kwargs.get("add_request_object", False) ++ if _add_request_object: ++ request_args["request"] = set_request_object(self, request_args) ++ ++ def update_service_context(self, resp, key="", **kwargs): ++ if "expires_in" in resp: ++ resp["__expires_at"] = time_sans_frac() + int(resp["expires_in"]) ++ self.upstream_get("context").cstate.update(key, resp) ++ ++ def store_auth_request(self, request_args=None, **kwargs): ++ """Store the authorization request in the state DB.""" ++ _key = get_state_parameter(request_args, kwargs) ++ self.upstream_get("context").cstate.update(_key, request_args) ++ return request_args ++ ++ def gather_request_args(self, **kwargs): ++ ar_args = Service.gather_request_args(self, **kwargs) ++ ++ if "redirect_uri" not in ar_args: ++ try: ++ ar_args["redirect_uri"] = self.upstream_get("context").get_usage("redirect_uris")[0] ++ except (KeyError, AttributeError): ++ raise MissingParameter("redirect_uri") ++ ++ return ar_args ++ ++ def post_parse_response(self, response, **kwargs): ++ """ ++ Add scope claim to response, from the request, if not present in the ++ response ++ ++ :param response: The response ++ :param kwargs: Extra Keyword arguments ++ :return: A possibly augmented response ++ """ ++ ++ if "scope" not in response: ++ try: ++ _key = kwargs["state"] ++ except KeyError: ++ pass ++ else: ++ if _key: ++ item = self.upstream_get("context").cstate.get_set( ++ _key, message=oauth2.AuthorizationRequest ++ ) ++ try: ++ response["scope"] = item["scope"] ++ except KeyError: ++ pass ++ return response +diff --git a/src/idpyoidc/client/oauth2/registration.py b/src/idpyoidc/client/oauth2/registration.py +index 19da498..ba2ecab 100644 +--- a/src/idpyoidc/client/oauth2/registration.py ++++ b/src/idpyoidc/client/oauth2/registration.py +@@ -4,6 +4,7 @@ from cryptojwt import KeyJar + + from idpyoidc.client.entity import response_types_to_grant_types + from idpyoidc.client.service import Service ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message import oauth2 + from idpyoidc.message.oauth2 import ResponseMessage + +@@ -75,7 +76,7 @@ class Registration(Service): + _keyjar = self.upstream_get("attribute", "keyjar") + if _keyjar: + if _client_id not in _keyjar: +- _keyjar.import_jwks(_keyjar.export_jwks(True, ""), issuer_id=_client_id) ++ _keyjar = store_under_other_id(_keyjar, "", _client_id, True) + _client_secret = _context.get_usage("client_secret") + if _client_secret: + if not _keyjar: + +diff --git a/src/idpyoidc/client/oauth2/stand_alone_client.py b/src/idpyoidc/client/oauth2/stand_alone_client.py +index 8652f56..c456176 100644 +--- a/src/idpyoidc/client/oauth2/stand_alone_client.py ++++ b/src/idpyoidc/client/oauth2/stand_alone_client.py +@@ -18,6 +18,8 @@ from idpyoidc.client.oauth2.utils import pick_redirect_uri + from idpyoidc.exception import MessageException + from idpyoidc.exception import MissingRequiredAttribute + from idpyoidc.exception import NotForMe ++from idpyoidc.key_import import add_kb ++from idpyoidc.key_import import import_jwks_from_file + from idpyoidc.message import Message + from idpyoidc.message.oauth2 import ResponseMessage + from idpyoidc.message.oauth2 import is_error_message +@@ -90,10 +92,10 @@ class StandAloneClient(Client): + elif typ == "file": + for kty, _name in _spec.items(): + if kty == "jwks": +- _kj.import_jwks_from_file(_name, _context.get("issuer")) ++ _kj = import_jwks_from_file(_kj, _name, _context.get("issuer")) + elif kty == "rsa": # PEM file + _kb = keybundle_from_local_file(_name, "der", ["sig"]) +- _kj.add_kb(_context.get("issuer"), _kb) ++ _kj = add_kb(_kj, _context.get("issuer"), _kb) + else: + raise ValueError("Unknown provider JWKS type: {}".format(typ)) + +@@ -746,7 +748,12 @@ def load_registration_response(client, request_args=None): + + :param client: A :py:class:`idpyoidc.client.oidc.Client` instance + """ +- if not client.get_context().get_client_id(): ++ _client_id = getattr(client, "client_id", None) ++ if not _client_id: ++ _context = client.get_context() ++ _client_id = getattr(_context, "client_id", None) ++ ++ if not _client_id: + try: + response = client.do_request("registration", request_args=request_args) + except KeyError: +diff --git a/src/idpyoidc/client/oauth2/utils.py b/src/idpyoidc/client/oauth2/utils.py +index 254e1bd..819ff00 100644 +--- a/src/idpyoidc/client/oauth2/utils.py ++++ b/src/idpyoidc/client/oauth2/utils.py +@@ -2,6 +2,8 @@ import logging + from typing import Optional + from typing import Union + ++from cryptojwt import JWT ++ + from idpyoidc.client.defaults import DEFAULT_RESPONSE_MODE + from idpyoidc.client.service import Service + from idpyoidc.exception import MissingParameter +@@ -99,3 +101,19 @@ def set_state_parameter(request_args=None, **kwargs): + """Assigned a state value.""" + request_args["state"] = get_state_parameter(request_args, kwargs) + return request_args, {"state": request_args["state"]} ++ ++def set_request_object(service, request_args): ++ # construct a signed request object ++ _context = service.upstream_get("context") ++ if _context.keyjar: ++ _jwt = JWT(key_jar=_context.keyjar) ++ else: ++ _jwt = JWT(key_jar=service.upstream_get("attribute", "keyjar")) ++ ++ if isinstance(request_args, Message): ++ _request_object = _jwt.pack(request_args.to_dict()) ++ else: ++ _request_object = _jwt.pack(request_args) ++ ++ # construct the message body ++ return _request_object +\ No newline at end of file + + +diff --git a/src/idpyoidc/server/oauth2/add_on/dpop.py b/src/idpyoidc/server/oauth2/add_on/dpop.py +index 5148cfe..22f37d5 100644 +--- a/src/idpyoidc/server/oauth2/add_on/dpop.py ++++ b/src/idpyoidc/server/oauth2/add_on/dpop.py +@@ -1,3 +1,4 @@ ++import base64 + import logging + from hashlib import sha256 + from typing import Callable +@@ -5,16 +6,17 @@ from typing import Optional + from typing import Union + + from cryptojwt import as_unicode ++from cryptojwt import BadSyntax + from cryptojwt import JWS + from cryptojwt.jwk.jwk import key_from_jwk_dict + from cryptojwt.jws.jws import factory + ++from idpyoidc.alg_info import get_signing_algs + from idpyoidc.message import Message + from idpyoidc.message import SINGLE_OPTIONAL_STRING + from idpyoidc.message import SINGLE_REQUIRED_INT + from idpyoidc.message import SINGLE_REQUIRED_JSON + from idpyoidc.message import SINGLE_REQUIRED_STRING +-from idpyoidc.metadata import get_signing_algs + from idpyoidc.server.client_authn import BearerHeader + + logger = logging.getLogger(__name__) +@@ -107,7 +109,14 @@ def token_post_parse_request(request, client_id, context, **kwargs): + if not _http_info: + return request + +- _dpop = DPoPProof().verify_header(_http_info["headers"]["dpop"]) ++ _headers = _http_info['headers'] ++ logger.debug(f"http headers: {_headers}") ++ ++ _dpop_header = _headers.get("dpop", _headers.get("http_dpop", None)) ++ if not _dpop_header: ++ raise ValueError("Missing DPoP header") ++ ++ _dpop = DPoPProof().verify_header(_dpop_header) + + # The signature of the JWS is verified, now for checking the + # content +@@ -126,11 +135,25 @@ def token_post_parse_request(request, client_id, context, **kwargs): + return request + + ++def add_padding(b): ++ # add padding chars ++ m = len(b) % 4 ++ if m == 1: ++ # NOTE: for some reason b64decode raises *TypeError* if the ++ # padding is incorrect. ++ raise BadSyntax(b, "incorrect padding") ++ elif m == 2: ++ b += "==" ++ elif m == 3: ++ b += "=" ++ return b ++ ++ + def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs): + """ + Expect http_info attribute in kwargs. http_info should be a dictionary + containing HTTP information. +- This function is ment for DPoP-protected resources. ++ This function is meant for DPoP-protected resources. + + :param request: + :param client_id: +@@ -143,7 +166,18 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs + if not _http_info: + return request + +- _dpop = DPoPProof().verify_header(_http_info["headers"]["dpop"]) ++ _headers = _http_info.get("headers", "") ++ if _headers: ++ _dpop_header = _headers.get("dpop", "") ++ if not _dpop_header: ++ _dpop_header = _headers.get("http_dpop", "") ++ if not _dpop_header: ++ logger.debug(f"Request Headers: {_headers}") ++ raise ValueError("Expected DPoP header, none found") ++ else: ++ raise ValueError("Expected DPoP header, no headers found") ++ ++ _dpop = DPoPProof().verify_header(_dpop_header) + + # The signature of the JWS is verified, now for checking the + # content +@@ -157,10 +191,19 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs + if not _dpop.key: + _dpop.key = key_from_jwk_dict(_dpop["jwk"]) + +- ath = sha256(auth_info["token"].encode("utf8")).hexdigest() ++ _token = auth_info.get("token", None) ++ if _token: ++ # base64.urlsafe_b64encode(sha256(token.encode("utf8")).digest()) ++ ath = as_unicode(base64.urlsafe_b64encode(sha256(_token.encode("utf8")).digest())) + +- if _dpop["ath"] != ath: +- raise ValueError("'ath' in DPoP does not match the token hash") ++ _ath = _dpop.get("ath", None) ++ if _ath is None: ++ raise ValueError("'ath' missing from DPoP") ++ else: ++ _athb = _ath.rstrip("=") ++ _ath = add_padding(_athb) ++ if _ath != ath: ++ raise ValueError("'ath' in DPoP does not match the token hash") + + # Need something I can add as a reference when minting tokens + request["dpop_jkt"] = as_unicode(_dpop.key.thumbprint("SHA-256")) +@@ -184,31 +227,28 @@ def _add_to_context(endpoint, algs_supported): + _context = endpoint.upstream_get("context") + _context.provider_info["dpop_signing_alg_values_supported"] = algs_supported + _context.add_on["dpop"] = {"algs_supported": algs_supported} +- _context.client_authn_methods["dpop"] = DPoPClientAuth +- ++ _context.client_authn_methods["dpop"] = DPoPClientAuth(endpoint.upstream_get) + +-def add_support(endpoint: dict, **kwargs): +- # Pick the token endpoint +- _endp = endpoint.get("token", None) +- if _endp: +- _endp.post_parse_request.append(token_post_parse_request) +- _added_to_context = False + +- _algs_supported = kwargs.get("dpop_signing_alg_values_supported") +- if not _algs_supported: ++def add_support(endpoint: dict, dpop_signing_alg_values_supported=None, dpop_endpoints=None, ++ **kwargs): ++ if dpop_signing_alg_values_supported is None: + _algs_supported = ["RS256"] + else: +- _algs_supported = [alg for alg in _algs_supported if alg in get_signing_algs()] ++ # Pick out the ones I support ++ _algs_supported = [alg for alg in dpop_signing_alg_values_supported if ++ alg in get_signing_algs()] ++ ++ _added_to_context = False + +- if _endp: +- _add_to_context(_endp, _algs_supported) +- _added_to_context = True ++ if dpop_endpoints is None: ++ dpop_endpoints = ["userinfo"] + +- for _dpop_endpoint in kwargs.get("dpop_endpoints", ["userinfo"]): ++ for _dpop_endpoint in dpop_endpoints: + _endpoint = endpoint.get(_dpop_endpoint, None) + if _endpoint: + if not _added_to_context: +- _add_to_context(_endp, _algs_supported) ++ _add_to_context(_endpoint, _algs_supported) + _added_to_context = True + + _endpoint.post_parse_request.append(userinfo_post_parse_request) +@@ -220,7 +260,7 @@ def add_support(endpoint: dict, **kwargs): + class DPoPClientAuth(BearerHeader): + tag = "dpop_client_auth" + +- def is_usable(self, request=None, authorization_token=None, http_headers=None): ++ def is_usable(self, request=None, authorization_token=None, http_info=None): + if authorization_token is not None and authorization_token.startswith("DPoP "): + return True + return False +@@ -231,6 +271,7 @@ class DPoPClientAuth(BearerHeader): + authorization_token: Optional[str] = None, + endpoint=None, # Optional[Endpoint] + get_client_id_from_token: Optional[Callable] = None, ++ http_info: Optional[dict] = None, + **kwargs, + ): + # info contains token and client_id +diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py +index 0766af5..223c419 100755 +--- a/src/idpyoidc/server/oauth2/authorization.py ++++ b/src/idpyoidc/server/oauth2/authorization.py +@@ -4,9 +4,9 @@ from typing import List + from typing import Optional + from typing import TypeVar + from typing import Union ++from urllib.parse import parse_qs + from urllib.parse import ParseResult + from urllib.parse import SplitResult +-from urllib.parse import parse_qs + from urllib.parse import unquote + from urllib.parse import urlencode + from urllib.parse import urlparse +@@ -18,7 +18,7 @@ from cryptojwt.jws.exception import NoSuitableSigningKeys + from cryptojwt.utils import as_bytes + from cryptojwt.utils import b64e + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.exception import ImproperlyConfigured + from idpyoidc.exception import ParameterError + from idpyoidc.exception import URIError +@@ -48,7 +48,6 @@ from idpyoidc.time_util import utc_time_sans_frac + from idpyoidc.util import importer + from idpyoidc.util import rndstr + +- + ParsedURI = TypeVar('ParsedURI', ParseResult, SplitResult) + + logger = logging.getLogger(__name__) +@@ -192,6 +191,9 @@ def verify_uri( + # Separate the URL from the query string object for the requested redirect URI. + req_redirect_uri_query_obj = parse_qs(req_redirect_uri_obj.query) + req_redirect_uri_without_query_obj = req_redirect_uri_obj._replace(query=None) ++ logger.debug(f"req_redirect_uri_query_obj: {req_redirect_uri_query_obj}") ++ logger.debug(f"req_redirect_uri_without_query_obj: {req_redirect_uri_without_query_obj}") ++ logger.debug(f"client_redirect_uris_obj: {client_redirect_uris_obj}") + + match = any( + req_redirect_uri_without_query_obj == uri_obj +@@ -393,15 +395,16 @@ class Authorization(Endpoint): + _supports = { + "claims_parameter_supported": True, + "request_parameter_supported": True, +- "request_uri_parameter_supported": True, ++ "request_uri_parameter_supported": None, + "response_types_supported": ["code"], + "response_modes_supported": ["query", "fragment", "form_post"], +- "request_object_signing_alg_values_supported": metadata.get_signing_algs(), +- "request_object_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "request_object_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "request_object_encryption_alg_values_supported": [], ++ "request_object_encryption_enc_values_supported": [], + # "grant_types_supported": ["authorization_code", "implicit"], + "code_challenge_methods_supported": ["S256"], + "scopes_supported": [], ++ # "grant_types": [], + } + default_capabilities = { + "client_authn_method": ["request_param", "public"], +@@ -434,8 +437,15 @@ class Authorization(Endpoint): + # If no response_type is registered by the client then we'll use code. + _registered = [{"code"}] + ++ if isinstance(request["response_type"], list): ++ _set = set(request["response_type"]) ++ else: ++ _set = set() ++ _set.add(request["response_type"]) ++ logger.debug(f"Asked for response_type: {_set}") ++ logger.debug(f"Supported response_types: {_registered}") + # Is the asked for response_type among those that are permitted +- return set(request["response_type"]) in _registered ++ return _set in _registered + + def mint_token(self, token_class, grant, session_id, based_on=None, **kwargs): + usage_rules = grant.usage_rules.get(token_class, {}) +@@ -462,15 +472,18 @@ class Authorization(Endpoint): + def _do_request_uri(self, request, client_id, context, **kwargs): + _request_uri = request.get("request_uri") + if _request_uri: ++ logger.debug("Got a 'request_uri") + # Do I do pushed authorization requests ? + _endp = self.upstream_get("endpoint", "pushed_authorization") + if _endp: + # Is it a UUID urn + if _request_uri.startswith("urn:uuid:"): ++ logger.debug("It's a PAR request_uri") + _req = context.par_db.get(_request_uri) + if _req: + # One time usage + del context.par_db[_request_uri] ++ logger.debug(f"Restored request: {_req}") + return _req + else: + raise ValueError("Got a request_uri I can not resolve") +@@ -517,6 +530,7 @@ class Authorization(Endpoint): + request[k] = v + + request[verified_claim_name("request")] = _ver_request ++ logger.debug(f"Fetched request: {request}") + else: + raise ServiceError("Got a %s response", _resp.status) + +@@ -542,7 +556,7 @@ class Authorization(Endpoint): + + _cinfo = context.cdb.get(client_id) + if not _cinfo: +- logger.error("Client ID ({}) not in client database".format(request["client_id"])) ++ logger.error(f"Client ID ({request['client_id']}) not in client database") + return self.authentication_error_response( + request, error="unauthorized_client", error_description="unknown client" + ) +@@ -911,7 +925,7 @@ class Authorization(Endpoint): + + if isinstance(request["response_type"], list): + rtype = set(request["response_type"][:]) +- else: # assume it's a string ++ else: # assume it's a string + rtype = set() + rtype.add(request["response_type"]) + +@@ -1223,10 +1237,12 @@ class AllowedAlgorithms: + _allowed = _cinfo.get(_reg) + if _allowed is None: + _allowed = _pinfo.get(_sup) ++ if _allowed is None: ++ _allowed = [_pinfo.get(_reg)] + + if alg not in _allowed: +- logger.error("Signing alg user: {} not among allowed: {}".format(alg, _allowed)) +- raise ValueError("Not allowed '%s' algorithm used", alg) ++ logger.error(f"Signing alg user: {alg} not among allowed: {_allowed}") ++ raise ValueError(f"Not allowed {alg} algorithm used") + + + def re_authenticate(request, authn) -> bool: +diff --git a/src/idpyoidc/server/oauth2/pushed_authorization.py b/src/idpyoidc/server/oauth2/pushed_authorization.py +index 693b073..52b5616 100644 +--- a/src/idpyoidc/server/oauth2/pushed_authorization.py ++++ b/src/idpyoidc/server/oauth2/pushed_authorization.py +@@ -45,6 +45,6 @@ class PushedAuthorization(Authorization): + self.upstream_get("context").par_db[_urn] = _request + + return { +- "http_response": {"request_uri": _urn, "expires_in": self.ttl}, ++ "response_args": {"request_uri": _urn, "expires_in": self.ttl}, + "return_uri": _request["redirect_uri"], + } + diff --git a/patch/oidc.patch b/patch/oidc.patch new file mode 100644 index 00000000..34eb24e0 --- /dev/null +++ b/patch/oidc.patch @@ -0,0 +1,687 @@ + +diff --git a/src/idpyoidc/client/oidc/__init__.py b/src/idpyoidc/client/oidc/__init__.py +index 45cefe2..0ae936f 100755 +--- a/src/idpyoidc/client/oidc/__init__.py ++++ b/src/idpyoidc/client/oidc/__init__.py +@@ -81,18 +81,18 @@ class RP(oauth2.Client): + client_type = "oidc" + + def __init__( +- self, +- keyjar: Optional[KeyJar] = None, +- config: Optional[Union[dict, Configuration]] = None, +- services: Optional[dict] = None, +- httpc: Optional[Callable] = None, +- httpc_params: Optional[dict] = None, +- upstream_get: Optional[Callable] = None, +- key_conf: Optional[dict] = None, +- entity_id: Optional[str] = "", +- verify_ssl: Optional[bool] = True, +- jwks_uri: Optional[str] = "", +- **kwargs ++ self, ++ keyjar: Optional[KeyJar] = None, ++ config: Optional[Union[dict, Configuration]] = None, ++ services: Optional[dict] = None, ++ httpc: Optional[Callable] = None, ++ httpc_params: Optional[dict] = None, ++ upstream_get: Optional[Callable] = None, ++ key_conf: Optional[dict] = None, ++ entity_id: Optional[str] = "", ++ verify_ssl: Optional[bool] = True, ++ jwks_uri: Optional[str] = "", ++ **kwargs + ): + if services: + _srvs = services +diff --git a/src/idpyoidc/client/oidc/access_token.py b/src/idpyoidc/client/oidc/access_token.py +index af629fa..91736d5 100644 +--- a/src/idpyoidc/client/oidc/access_token.py ++++ b/src/idpyoidc/client/oidc/access_token.py +@@ -2,6 +2,7 @@ import logging + from typing import Optional + from typing import Union + ++from idpyoidc.alg_info import get_signing_algs + from idpyoidc.client.client_auth import get_client_authn_methods + from idpyoidc.client.exception import ParameterError + from idpyoidc.client.oauth2 import access_token +@@ -9,7 +10,6 @@ from idpyoidc.client.oidc import IDT2REG + from idpyoidc.message import Message + from idpyoidc.message import oidc + from idpyoidc.message.oidc import verified_claim_name +-from idpyoidc.metadata import get_signing_algs + from idpyoidc.time_util import time_sans_frac + + __author__ = "Roland Hedberg" +@@ -34,7 +34,8 @@ class AccessToken(access_token.AccessToken): + access_token.AccessToken.__init__(self, upstream_get, conf=conf) + + def gather_verify_arguments( +- self, response: Optional[Union[dict, Message]] = None, behaviour_args: Optional[dict] = None ++ self, response: Optional[Union[dict, Message]] = None, ++ behaviour_args: Optional[dict] = None + ): + """ + Need to add some information before running verify() +diff --git a/src/idpyoidc/client/oidc/authorization.py b/src/idpyoidc/client/oidc/authorization.py +index 03cde13..e13da13 100644 +--- a/src/idpyoidc/client/oidc/authorization.py ++++ b/src/idpyoidc/client/oidc/authorization.py +@@ -3,22 +3,20 @@ from typing import List + from typing import Optional + from typing import Union + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.client.oauth2 import authorization + from idpyoidc.client.oauth2.utils import pre_construct_pick_redirect_uri + from idpyoidc.client.oidc import IDT2REG +-from idpyoidc.client.oidc.utils import construct_request_uri +-from idpyoidc.client.oidc.utils import request_object_encryption ++from idpyoidc.client.request_object import construct_request_parameter ++from idpyoidc.client.request_object import construct_request_uri + from idpyoidc.client.service_context import ServiceContext + from idpyoidc.client.util import implicit_response_types + from idpyoidc.exception import MissingRequiredAttribute + from idpyoidc.message import Message + from idpyoidc.message import oauth2 + from idpyoidc.message import oidc +-from idpyoidc.message.oidc import make_openid_request + from idpyoidc.message.oidc import verified_claim_name + from idpyoidc.time_util import time_sans_frac +-from idpyoidc.time_util import utc_time_sans_frac + from idpyoidc.util import rndstr + + __author__ = "Roland Hedberg" +@@ -32,11 +30,11 @@ class Authorization(authorization.Authorization): + error_msg = oidc.ResponseMessage + + _supports = { +- "request_object_signing_alg_values_supported": metadata.get_signing_algs(), +- "request_object_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "request_object_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "request_object_encryption_alg_values_supported": alg_info.get_encryption_algs(), ++ "request_object_encryption_enc_values_supported": alg_info.get_encryption_encs(), + "response_types_supported": ["code", "id_token", "code id_token"], +- "request_parameter_supported": None, ++ "request_parameter_supported": True, + "request_uri_parameter_supported": None, + "request_uris": None, + "request_parameter": None, +@@ -212,63 +210,6 @@ class Authorization(authorization.Authorization): + fid.close() + return _webname + +- def construct_request_parameter( +- self, req, request_param, audience=None, expires_in=0, **kwargs +- ): +- """Construct a request parameter""" +- alg = self.get_request_object_signing_alg(**kwargs) +- kwargs["request_object_signing_alg"] = alg +- +- _context = self.upstream_get("context") +- if "keys" not in kwargs and alg and alg != "none": +- kwargs["keys"] = self.upstream_get("attribute", "keyjar") +- +- if alg == "none": +- kwargs["keys"] = [] +- +- # This is the issuer of the JWT, that is me ! +- _issuer = kwargs.get("issuer") +- if _issuer is None: +- kwargs["issuer"] = _context.get_client_id() +- +- if kwargs.get("recv") is None: +- try: +- kwargs["recv"] = _context.provider_info["issuer"] +- except KeyError: +- kwargs["recv"] = _context.issuer +- +- try: +- del kwargs["service"] +- except KeyError: +- pass +- +- if expires_in: +- req["exp"] = utc_time_sans_frac() + int(expires_in) +- +- _mor_args = { +- k: kwargs[k] +- for k in [ +- "keys", +- "issuer", +- "request_object_signing_alg", +- "recv", +- "with_jti", +- "lifetime", +- ] +- if k in kwargs +- } +- +- _req_jwt = make_openid_request(req, **_mor_args) +- +- if "target" not in kwargs: +- kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) +- +- # Should the request be encrypted +- _req_jwte = request_object_encryption( +- _req_jwt, _context, self.upstream_get("attribute", "keyjar"), **kwargs +- ) +- return _req_jwte +- + def oidc_post_construct(self, req, **kwargs): + """ + Modify the request arguments. +@@ -303,10 +244,21 @@ class Authorization(authorization.Authorization): + if _request_param == "request_uri": + kwargs["base_path"] = _context.get("base_url") + "/" + "requests" + kwargs["local_dir"] = _context.get_usage("requests_dir", "./requests") +- _req = self.construct_request_parameter(req, _request_param, **kwargs) ++ service = kwargs.get("service") ++ if service: ++ del kwargs["service"] ++ else: ++ service = self ++ ++ _req = construct_request_parameter(service, req, _request_param, **kwargs) + req["request_uri"] = self.store_request_on_file(_req, **kwargs) + elif _request_param == "request": +- _req = self.construct_request_parameter(req, _request_param, **kwargs) ++ service = kwargs.get("service") ++ if service: ++ del kwargs["service"] ++ else: ++ service = self ++ _req = construct_request_parameter(service, req, _request_param, **kwargs) + req["request"] = _req + + if _req: +@@ -319,7 +271,8 @@ class Authorization(authorization.Authorization): + return req + + def gather_verify_arguments( +- self, response: Optional[Union[dict, Message]] = None, behaviour_args: Optional[dict] = None ++ self, response: Optional[Union[dict, Message]] = None, ++ behaviour_args: Optional[dict] = None + ): + """ + Need to add some information before running verify() +@@ -379,12 +332,12 @@ class Authorization(authorization.Authorization): + return "" + + def construct_uris( +- self, +- base_url: str, +- hex: bytes, +- context: ServiceContext, +- targets: Optional[List[str]] = None, +- response_types: Optional[List[str]] = None, ++ self, ++ base_url: str, ++ hex: bytes, ++ context: ServiceContext, ++ targets: Optional[List[str]] = None, ++ response_types: Optional[List[str]] = None, + ): + _callback_uris = context.get_preference("callback_uris", {}) + +diff --git a/src/idpyoidc/client/oidc/registration.py b/src/idpyoidc/client/oidc/registration.py +index 4933905..5c4fef9 100644 +--- a/src/idpyoidc/client/oidc/registration.py ++++ b/src/idpyoidc/client/oidc/registration.py +@@ -4,6 +4,7 @@ from cryptojwt import KeyJar + + from idpyoidc.client.entity import response_types_to_grant_types + from idpyoidc.client.service import Service ++from idpyoidc.key_import import import_jwks + from idpyoidc.message import oidc + from idpyoidc.message.oauth2 import ResponseMessage + +@@ -75,7 +76,7 @@ class Registration(Service): + _keyjar = self.upstream_get("attribute", "keyjar") + if _keyjar: + if _client_id not in _keyjar: +- _keyjar.import_jwks(_keyjar.export_jwks(True, ""), issuer_id=_client_id) ++ _keyjar= import_jwks(_keyjar, _keyjar.export_jwks(True, ""), _client_id) + _client_secret = _context.get_usage("client_secret") + if _client_secret: + if not _keyjar: +@@ -102,7 +103,8 @@ class Registration(Service): + @return: + """ + _context = self.upstream_get("context") +- req_args = _context.claims.create_registration_request() ++ req_args = _context.claims.get_client_metadata(metadata_schema=self.msg_type, ++ supported=_context.supports()) + if "request_args" in self.conf: + req_args.update(self.conf["request_args"]) + +diff --git a/src/idpyoidc/client/oidc/userinfo.py b/src/idpyoidc/client/oidc/userinfo.py +index 05fce76..b92410d 100644 +--- a/src/idpyoidc/client/oidc/userinfo.py ++++ b/src/idpyoidc/client/oidc/userinfo.py +@@ -8,9 +8,9 @@ from idpyoidc.client.service import Service + from idpyoidc.exception import MissingSigningKey + from idpyoidc.message import Message + from idpyoidc.message import oidc +-from idpyoidc.metadata import get_encryption_algs +-from idpyoidc.metadata import get_encryption_encs +-from idpyoidc.metadata import get_signing_algs ++from idpyoidc.alg_info import get_encryption_algs ++from idpyoidc.alg_info import get_encryption_encs ++from idpyoidc.alg_info import get_signing_algs + + logger = logging.getLogger(__name__) + +diff --git a/src/idpyoidc/client/oidc/utils.py b/src/idpyoidc/client/oidc/utils.py +index 2b428fe..e69de29 100644 +--- a/src/idpyoidc/client/oidc/utils.py ++++ b/src/idpyoidc/client/oidc/utils.py +@@ -1,85 +0,0 @@ +-import os +- +-from cryptojwt.jwe.jwe import JWE +-from cryptojwt.jwe.utils import alg2keytype +- +-from idpyoidc.exception import MissingRequiredAttribute +-from idpyoidc.util import rndstr +- +- +-def request_object_encryption(msg, service_context, keyjar, **kwargs): +- """ +- Created an encrypted JSON Web token with *msg* as body. +- +- :param msg: The mesaqg +- :param service_context: +- :param kwargs: +- :return: +- """ +- try: +- encalg = kwargs["request_object_encryption_alg"] +- except KeyError: +- try: +- encalg = service_context.get_usage("request_object_encryption_alg") +- except KeyError: +- return msg +- +- if not encalg: +- return msg +- +- try: +- encenc = kwargs["request_object_encryption_enc"] +- except KeyError: +- try: +- encenc = service_context.get_usage("request_object_encryption_enc") +- except KeyError: +- raise MissingRequiredAttribute("No request_object_encryption_enc specified") +- +- if not encenc: +- raise MissingRequiredAttribute("No request_object_encryption_enc specified") +- +- _jwe = JWE(msg, alg=encalg, enc=encenc) +- _kty = alg2keytype(encalg) +- +- try: +- _kid = kwargs["enc_kid"] +- except KeyError: +- _kid = "" +- +- _target = kwargs.get("target", kwargs.get("recv", None)) +- if _target is None: +- raise MissingRequiredAttribute("No target specified") +- +- if _kid: +- _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target, kid=_kid) +- _jwe["kid"] = _kid +- else: +- _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target) +- +- return _jwe.encrypt(_keys) +- +- +-def construct_request_uri(local_dir, base_path, **kwargs): +- """ +- Constructs a special redirect_uri to be used when communicating with +- one OP. Each OP should get their own redirect_uris. +- +- :param local_dir: Local directory in which to place the file +- :param base_path: Base URL to start with +- :param kwargs: +- :return: 2-tuple with (filename, url) +- """ +- _filedir = local_dir +- if not os.path.isdir(_filedir): +- os.makedirs(_filedir) +- _webpath = base_path +- _name = rndstr(10) + ".jwt" +- filename = os.path.join(_filedir, _name) +- while os.path.exists(filename): +- _name = rndstr(10) +- filename = os.path.join(_filedir, _name) +- if _webpath.endswith("/"): +- _webname = f"{_webpath}{_name}" +- else: +- _webname = f"{_webpath}/{_name}" +- return filename, _webname + +diff --git a/src/idpyoidc/server/oidc/authorization.py b/src/idpyoidc/server/oidc/authorization.py +index e6daad3..fc4852e 100644 +--- a/src/idpyoidc/server/oidc/authorization.py ++++ b/src/idpyoidc/server/oidc/authorization.py +@@ -2,7 +2,7 @@ import logging + from typing import Callable + from urllib.parse import urlsplit + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.message import oidc + from idpyoidc.message.oidc import Claims + from idpyoidc.message.oidc import verified_claim_name +@@ -82,11 +82,11 @@ class Authorization(authorization.Authorization): + **{ + "claims_parameter_supported": True, + "encrypt_request_object_supported": False, +- "request_object_signing_alg_values_supported": metadata.get_signing_algs(), +- "request_object_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "request_object_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "request_object_encryption_alg_values_supported": [], ++ "request_object_encryption_enc_values_supported": [], + "request_parameter_supported": True, +- "request_uri_parameter_supported": True, ++ "request_uri_parameter_supported": False, + "require_request_uri_registration": False, + "response_types_supported": ["code", "id_token", "code id_token"], + "response_modes_supported": ["query", "fragment", "form_post"], +diff --git a/src/idpyoidc/server/oidc/backchannel_authentication.py b/src/idpyoidc/server/oidc/backchannel_authentication.py +index b193e22..c45a980 100644 +--- a/src/idpyoidc/server/oidc/backchannel_authentication.py ++++ b/src/idpyoidc/server/oidc/backchannel_authentication.py +@@ -86,10 +86,10 @@ class BackChannelAuthentication(Endpoint): + return set(res) + + def process_request( +- self, +- request: Optional[Union[Message, dict]] = None, +- http_info: Optional[dict] = None, +- **kwargs, ++ self, ++ request: Optional[Union[Message, dict]] = None, ++ http_info: Optional[dict] = None, ++ **kwargs, + ): + try: + request_user = self.do_request_user(request) +@@ -125,6 +125,7 @@ class BackChannelAuthentication(Endpoint): + + + class CIBATokenHelper(AccessTokenHelper): ++ + def _get_session_info(self, request, session_manager): + _path = request["_session_path"] + _grant = session_manager.get(_path) +@@ -137,7 +138,7 @@ class CIBATokenHelper(AccessTokenHelper): + return session_info, _grant + + def post_parse_request( +- self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ++ self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + ) -> Union[Message, dict]: + _context = self.endpoint.upstream_get("context") + _mngr = _context.session_manager +@@ -303,10 +304,10 @@ class ClientNotification(Endpoint): + Endpoint.__init__(self, upstream_get, **kwargs) + + def process_request( +- self, +- request: Optional[Union[Message, dict]] = None, +- http_info: Optional[dict] = None, +- **kwargs, ++ self, ++ request: Optional[Union[Message, dict]] = None, ++ http_info: Optional[dict] = None, ++ **kwargs, + ) -> Union[Message, dict]: + return {} + +@@ -316,17 +317,18 @@ class ClientNotificationAuthn(ClientSecretBasic): + + tag = "client_notification_authn" + +- def is_usable(self, request=None, authorization_token=None): ++ def is_usable(self, request=None, authorization_token=None, http_info=None): + if authorization_token is not None and authorization_token.startswith("Bearer "): + return True + return False + + def _verify( +- self, +- authorization_token: Optional[str] = None, +- endpoint=None, # Optional[Endpoint] +- get_client_id_from_token: Optional[Callable] = None, +- **kwargs, ++ self, ++ authorization_token: Optional[str] = None, ++ endpoint=None, # Optional[Endpoint] ++ get_client_id_from_token: Optional[Callable] = None, ++ http_info: Optional[dict] = None, ++ **kwargs, + ): + ttype, token = authorization_token.split(" ", 1) + if ttype != "Bearer": +diff --git a/src/idpyoidc/server/oidc/provider_config.py b/src/idpyoidc/server/oidc/provider_config.py +index 819a699..374ebde 100755 +--- a/src/idpyoidc/server/oidc/provider_config.py ++++ b/src/idpyoidc/server/oidc/provider_config.py +@@ -33,4 +33,14 @@ class ProviderConfiguration(Endpoint): + return request + + def process_request(self, request=None, **kwargs): +- return {"response_args": self.upstream_get("context").provider_info} ++ # return {"response_args": self.upstream_get("context").provider_info} ++ _schema = self.upstream_get("attribute", "metadata_schema") ++ _args = self.upstream_get("context").claims.get_server_metadata(metadata_schema=_schema) ++ # add issuer ++ _args["issuer"] = self.upstream_get("attribute", "entity_id") ++ # add endpoints ++ for name, endpoint in self.upstream_get("unit").endpoint.items(): ++ if endpoint.endpoint_name: ++ _args[endpoint.endpoint_name] = endpoint.full_path ++ ++ return {"response_args": _args} +diff --git a/src/idpyoidc/server/oidc/registration.py b/src/idpyoidc/server/oidc/registration.py +index a363ebe..2ae23cb 100644 +--- a/src/idpyoidc/server/oidc/registration.py ++++ b/src/idpyoidc/server/oidc/registration.py +@@ -12,6 +12,8 @@ from cryptojwt.jws.utils import alg2keytype + from cryptojwt.utils import as_bytes + + from idpyoidc.exception import MessageException ++from idpyoidc.key_import import import_jwks ++from idpyoidc.key_import import import_jwks_as_json + from idpyoidc.message.oauth2 import ResponseMessage + from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB +@@ -143,7 +145,7 @@ class Registration(Endpoint): + # Use my defaults + _my_key = _context.claims.register2preferred.get(claim, claim) + try: +- _val = _context.provider_info[_my_key] ++ _val = _context.claims.get_preference(_my_key) + except KeyError: + return val + +@@ -279,14 +281,23 @@ class Registration(Endpoint): + + t = {"jwks_uri": "", "jwks": None} + +- for item in ["jwks_uri", "jwks"]: +- if item in request: +- t[item] = request[item] ++ _jwks_uri = request.get("jwks_uri") ++ if _jwks_uri: ++ # if it can't load keys because the URL is false it will ++ # just silently fail. Waiting for better times. ++ _keyjar.add_url(issuer_id=client_id, url=_jwks_uri) ++ else: ++ _jwks = request.get("jwks", None) ++ if _jwks: ++ if isinstance(_jwks, str): ++ _keyjar = import_jwks_as_json(_keyjar, _jwks, client_id) ++ else: ++ _keyjar = import_jwks(_keyjar, _jwks, client_id) + +- # if it can't load keys because the URL is false it will +- # just silently fail. Waiting for better times. +- _keyjar.load_keys(client_id, jwks_uri=t["jwks_uri"], jwks=t["jwks"]) +- logger.debug(f"Keys for {client_id}: {_keyjar.key_summary(client_id)}") ++ if client_id in _keyjar: ++ logger.debug(f"Keys for {client_id}: {_keyjar.key_summary(client_id)}") ++ else: ++ logger.debug(f"No keys for {client_id}") + + return _cinfo + +@@ -437,7 +448,13 @@ class Registration(Endpoint): + if not reserved_client_id: + reserved_client_id = _context.cdb.keys() + client_id = cid_generator(reserved=reserved_client_id, **cid_gen_kwargs) +- if "client_id" in request: ++ _entity_id = request.get("client_id", None) ++ if _entity_id: ++ # Already registered ++ _old_id = _context.client_known_as.get(request["client_id"], None) ++ if _old_id: ++ del _context.cdb[_old_id] ++ _context.client_known_as[_entity_id] = client_id + del request["client_id"] + else: + client_id = request.get("client_id") +@@ -456,7 +473,7 @@ class Registration(Endpoint): + if set_secret: + client_secret = self.add_client_secret(_cinfo, client_id, _context) + +- logger.debug("Stored client info in CDB under cid={}".format(client_id)) ++ logger.debug(f"Stored client info in CDB under cid={client_id}") + + _context.cdb[client_id] = _cinfo + _cinfo = self.do_client_registration( +@@ -469,6 +486,12 @@ class Registration(Endpoint): + + args = dict([(k, v) for k, v in _cinfo.items() if k in self.response_cls.c_param]) + ++ # Don't echo keys back ++ try: ++ del args["jwks"] ++ except KeyError: ++ pass ++ + comb_uri(args) + response = self.response_cls(**args) + +@@ -495,7 +518,7 @@ class Registration(Endpoint): + reg_resp = self.client_registration_setup(request, new_id, set_secret, + reserved_client_id) + except Exception as err: +- logger.error("client_registration_setup: %s", request) ++ logger.exception(f"client_registration_setup: {request}") + return ResponseMessage( + error="invalid_configuration_request", error_description="%s" % err + ) +diff --git a/src/idpyoidc/server/oidc/session.py b/src/idpyoidc/server/oidc/session.py +index 03ddfd2..f38e011 100644 +--- a/src/idpyoidc/server/oidc/session.py ++++ b/src/idpyoidc/server/oidc/session.py +@@ -135,7 +135,11 @@ class Session(Endpoint): + try: + alg = cinfo["id_token_signed_response_alg"] + except KeyError: +- alg = _context.provider_info["id_token_signing_alg_values_supported"][0] ++ _algs = _context.provider_info.get("id_token_signing_alg_values_supported") ++ if _algs: ++ alg = _algs[0] ++ else: ++ alg = _context.provider_info.get("id_token_signed_response_alg", "RS256") + + _jws = JWT( + self.upstream_get("attribute", "keyjar"), +diff --git a/src/idpyoidc/server/oidc/token.py b/src/idpyoidc/server/oidc/token.py +index 3436df3..804fa59 100755 +--- a/src/idpyoidc/server/oidc/token.py ++++ b/src/idpyoidc/server/oidc/token.py +@@ -1,6 +1,6 @@ + import logging + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.message import Message + from idpyoidc.message import oidc + from idpyoidc.message.oidc import TokenErrorResponse +@@ -40,7 +40,7 @@ class Token(token.Token): + "client_secret_jwt", + "private_key_jwt", + ], +- "token_endpoint_auth_signing_alg_values_supported": metadata.get_signing_algs(), ++ "token_endpoint_auth_signing_alg_values_supported": alg_info.get_signing_algs(), + "grant_types_supported": list(helper_by_grant_type.keys()), + } + +diff --git a/src/idpyoidc/server/oidc/token_helper/access_token.py b/src/idpyoidc/server/oidc/token_helper/access_token.py +index 2594748..eefc4e2 100755 +--- a/src/idpyoidc/server/oidc/token_helper/access_token.py ++++ b/src/idpyoidc/server/oidc/token_helper/access_token.py +@@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) + + + class AccessTokenHelper(TokenEndpointHelper): ++ + def _get_session_info(self, request, session_manager): + if request["grant_type"] != "authorization_code": + return self.error_cls(error="invalid_request", error_description="Unknown grant_type") +@@ -56,7 +57,7 @@ class AccessTokenHelper(TokenEndpointHelper): + if "grant_types_supported" in _context.cdb[client_id]: + grant_types_supported = _context.cdb[client_id].get("grant_types_supported") + else: +- grant_types_supported = _context.provider_info["grant_types_supported"] ++ grant_types_supported = _context.provider_info.get("grant_types", []) + grant = _session_info["grant"] + + token_type = "Bearer" +@@ -166,7 +167,7 @@ class AccessTokenHelper(TokenEndpointHelper): + return _response + + def post_parse_request( +- self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ++ self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + ) -> Union[Message, dict]: + """ + This is where clients come to get their access tokens +diff --git a/src/idpyoidc/server/oidc/userinfo.py b/src/idpyoidc/server/oidc/userinfo.py +index 281b669..6754f37 100755 +--- a/src/idpyoidc/server/oidc/userinfo.py ++++ b/src/idpyoidc/server/oidc/userinfo.py +@@ -9,7 +9,7 @@ from cryptojwt.exception import MissingValue + from cryptojwt.jwt import JWT + from cryptojwt.jwt import utc_time_sans_frac + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.exception import ImproperlyConfigured + from idpyoidc.message import Message + from idpyoidc.message import oidc +@@ -35,9 +35,9 @@ class UserInfo(Endpoint): + _supports = { + "claim_types_supported": ["normal", "aggregated", "distributed"], + "encrypt_userinfo_supported": True, +- "userinfo_signing_alg_values_supported": metadata.get_signing_algs(), +- "userinfo_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "userinfo_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "userinfo_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "userinfo_encryption_alg_values_supported": alg_info.get_encryption_algs(), ++ "userinfo_encryption_enc_values_supported": alg_info.get_encryption_encs(), + } + + def __init__( + diff --git a/patch/test_client_30.patch b/patch/test_client_30.patch new file mode 100644 index 00000000..0cb6c012 --- /dev/null +++ b/patch/test_client_30.patch @@ -0,0 +1,158 @@ +diff --git a/tests/test_client_30_rp_handler_oidc.py b/tests/test_client_30_rp_handler_oidc.py +index 3a3d75f..f02aa8d 100644 +--- a/tests/test_client_30_rp_handler_oidc.py ++++ b/tests/test_client_30_rp_handler_oidc.py +@@ -4,12 +4,13 @@ from urllib.parse import parse_qs + from urllib.parse import urlparse + from urllib.parse import urlsplit + +-from cryptojwt.key_jar import init_key_jar + import pytest + import responses ++from cryptojwt.key_jar import init_key_jar + + from idpyoidc.client.entity import Entity + from idpyoidc.client.rp_handler import RPHandler ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oidc import AccessTokenResponse + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + from idpyoidc.message.oidc import AuthorizationResponse +@@ -217,6 +218,7 @@ def iss_id(iss): + + + class TestRPHandler(object): ++ + @pytest.fixture(autouse=True) + def rphandler_setup(self): + self.rph = RPHandler( +@@ -270,6 +272,7 @@ class TestRPHandler(object): + 'id_token_signing_alg_values_supported', + 'redirect_uris', + 'request_object_signing_alg_values_supported', ++ 'request_parameter_supported', + 'response_modes_supported', + 'response_types_supported', + 'scopes_supported', +@@ -279,13 +282,13 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + # The key jar should only contain a symmetric key that is the clients + # secret. 2 because one is marked for encryption and the other signing + # usage. + +- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} ++ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} + keys = _keyjar.get_issuer_keys("") + assert len(keys) == 3 + +@@ -329,9 +332,9 @@ class TestRPHandler(object): + assert _context.issuer == _github_id + + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + +- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} ++ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} + keys = _keyjar.get_issuer_keys("") + assert len(keys) == 3 + +@@ -347,7 +350,7 @@ class TestRPHandler(object): + cb = _context.get_preference("callback_uris") + + assert set(cb.keys()) == {"request_uris", "redirect_uris"} +- assert set(cb["redirect_uris"].keys()) == {"query", "fragment"} ++ assert set(cb["redirect_uris"].keys()) == {"query", "fragment", "form_post"} + _hash = _context.iss_hash + + assert cb["redirect_uris"]["query"] == [f"https://example.com/rp/authz_cb/{_hash}"] +@@ -449,7 +452,7 @@ class TestRPHandler(object): + _github_id = iss_id("github") + _context = client.get_context() + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + _nonce = _session["nonce"] + _iss = _session["iss"] +@@ -524,7 +527,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -571,7 +574,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -618,7 +621,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -697,6 +700,7 @@ def test_get_provider_specific_service(): + + + class TestRPHandlerTier2(object): ++ + @pytest.fixture(autouse=True) + def rphandler_setup(self): + self.rph = RPHandler(BASE_URL, CLIENT_CONFIG, keyjar=CLI_KEY) +@@ -712,7 +716,7 @@ class TestRPHandlerTier2(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -818,6 +822,7 @@ class TestRPHandlerTier2(object): + + + class MockResponse: ++ + def __init__(self, status_code, text, headers=None): + self.status_code = status_code + self.text = text +@@ -825,6 +830,7 @@ class MockResponse: + + + class MockOP(object): ++ + def __init__(self, issuer, keyjar=None): + self.keyjar = keyjar + self.issuer = issuer +@@ -913,6 +919,7 @@ def test_rphandler_request(): + + + class TestRPHandlerWithMockOP(object): ++ + @pytest.fixture(autouse=True) + def rphandler_setup(self): + self.issuer = "https://github.com/login/oauth/authorize" +@@ -956,7 +963,7 @@ class TestRPHandlerWithMockOP(object): + ) + _github_id = iss_id("github") + _keyjar = client.get_attribute("keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + with responses.RequestsMock() as rsps: + rsps.add( + "POST", diff --git a/patch/tests.patch b/patch/tests.patch new file mode 100644 index 00000000..d745f2d1 --- /dev/null +++ b/patch/tests.patch @@ -0,0 +1,5084 @@ + + + + + + +diff --git a/tests/op_config.json b/tests/op_config.json +index 3d0da0b..9ea2da4 100644 +--- a/tests/op_config.json ++++ b/tests/op_config.json +@@ -44,7 +44,7 @@ + } + } + }, +- "capabilities": { ++ "preference": { + "subject_types_supported": [ + "public", + "pairwise" +diff --git a/tests/private/cookie_jwks.json b/tests/private/cookie_jwks.json +index 9d47588..5b507b7 100644 +--- a/tests/private/cookie_jwks.json ++++ b/tests/private/cookie_jwks.json +@@ -1 +1 @@ +-{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "4L_0vvQ5QsJvswvh5qCNFyLF4BTSI6xf"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "UsJ7o_W_ND7aoKnbeWEes3MJOECMMY_c"}]} +\ No newline at end of file ++{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "GpKOJkB-QVo3qV2FZMVZFvha-TyJTHeH"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "ugxh7wUNKyolAiXiEWFVL_BVcjaNxvvb"}]} +\ No newline at end of file +diff --git a/tests/private/token_jwks.json b/tests/private/token_jwks.json +index d3e0f07..d171cfa 100644 +--- a/tests/private/token_jwks.json ++++ b/tests/private/token_jwks.json +@@ -1 +1 @@ +-{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "vrjoMrmgK8SmJJPc318zTxqG_tvBqF5l"}]} +\ No newline at end of file ++{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "rJGcHkBJrCCUYp5k62ABrQuUeug_gmL6"}]} +\ No newline at end of file +diff --git a/tests/request123456.jwt b/tests/request123456.jwt +index fed3886..5338a28 100644 +--- a/tests/request123456.jwt ++++ b/tests/request123456.jwt +@@ -1 +1 @@ +-eyJhbGciOiJSUzI1NiIsImtpZCI6IlNIRXlZV2N3TlZrMExUZFJPVFp6WjJGVVduZElWWGRhY2sweFdVTTVTRXB3Y1MwM2RWVXhXVTR6UlEifQ.eyJyZXNwb25zZV90eXBlIjogImNvZGUiLCAic3RhdGUiOiAic3RhdGUiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vZXhhbXBsZS5jb20vY2xpL2F1dGh6X2NiIiwgInNjb3BlIjogIm9wZW5pZCIsICJub25jZSI6ICJpcno4SG5ELXFsOVhNYVJYUll1S3BpcEpHM2hiRWZ5akxBYXQwMjNLZEdvIiwgImNsaWVudF9pZCI6ICJjbGllbnRfaWQiLCAiaXNzIjogImNsaWVudF9pZCIsICJpYXQiOiAxNzEwODM2MDQwLCAiYXVkIjogWyJodHRwczovL2V4YW1wbGUuY29tIl19.EDvgPn7QJFm6O4d9QFU9gVZEmAREDIfl1RTiMtec7_ZJ4vGag3dxCyXgz15GbDrQgo6mqCydCe-Mal_4HBlRwMctqhy9NMIGM5PxIKzrqMjsk88jxAoz-WWw3I-pKrJUS4m23mEgLZkGQpB1N3YgO_RhG-7vGCkiJd_8VuomRMd2dX5_Jax3j12T7vhM_TUI9S6XJ5zsLn2ZOPQVXfoprr7HHY6UJjJ65Fp_hoGA3gmfJiHwbxYss8D2X1BNoLmEMze_e6cS-DGe648t2U47E77BvHdzsKi791Y1L3eizkm364gJ371KWbi3avvbSkTi4hEd3OikkyeMQZk6vDiJww +\ No newline at end of file ++eyJhbGciOiJSUzI1NiIsImtpZCI6IlNIRXlZV2N3TlZrMExUZFJPVFp6WjJGVVduZElWWGRhY2sweFdVTTVTRXB3Y1MwM2RWVXhXVTR6UlEifQ.eyJyZXNwb25zZV90eXBlIjogImNvZGUiLCAic3RhdGUiOiAic3RhdGUiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vZXhhbXBsZS5jb20vY2xpL2F1dGh6X2NiIiwgInNjb3BlIjogIm9wZW5pZCIsICJub25jZSI6ICIxcGNuQ3RTSHlrc1JwVzE4N2NWS2p0X2JjTUN0S0dGZzBOZTFyYlhlRXZzIiwgImNsaWVudF9pZCI6ICJjbGllbnRfaWQiLCAiZXhwIjogMTczMDM3NzA5MiwgImlzcyI6ICJjbGllbnRfaWQiLCAiaWF0IjogMTczMDM3MzQ5MiwgImF1ZCI6IFsicmVxdWVzdF91cmkiXSwgImp0aSI6ICI1NzUxZDhmNTVkYzU0OTE1OTI4MjhiMjM0M2YxZTMxZSJ9.k2D_v7jv9cFWIcB6I3HlUPnZg4Tx9FBANR4kPH16fh8cI-c_YFvbCfrXpwUOj1CUY4ZdFqtZnYQD8rQHtjqgXjF79X8H6V6c_7GjQSzHlSAPshFMGm4eXiDKfSCq1xu4YEC-qaub19JjqHpzq6y2Sfz1ayI5qdg_-yJap8HHoPSYzaZ_oPVP7u1TAP24fI15w_leSlgYuXFyzbCWlcWjoHxCJaxYobw3HrAJE4p9h5XL84Rth73xi918CuGw4ngWcF7aQg5dUc1HZPrefU0iVs7Rhi75GmkQzByx7kqIN0T1J-wUod7o69sIqrfWuccam3ndo5E8YlUvwXo0JwKlFQ +\ No newline at end of file +diff --git a/tests/test_04_message.py b/tests/test_04_message.py +index 7fe1878..a3c05b7 100644 +--- a/tests/test_04_message.py ++++ b/tests/test_04_message.py +@@ -14,6 +14,7 @@ from cryptojwt.key_jar import build_keyjar + from idpyoidc.exception import DecodeError + from idpyoidc.exception import MessageException + from idpyoidc.exception import OidcMsgError ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message import OPTIONAL_LIST_OF_MESSAGES + from idpyoidc.message import OPTIONAL_LIST_OF_STRINGS + from idpyoidc.message import OPTIONAL_MESSAGE +@@ -48,13 +49,13 @@ keym = [ + KEYJAR = build_keyjar(keys) + + IKEYJAR = build_keyjar(keys) +-IKEYJAR.import_jwks(IKEYJAR.export_jwks(private=True), "issuer") ++IKEYJAR = store_under_other_id(IKEYJAR, "", "issuer", True) + del IKEYJAR[""] + + KEYJARS = {} + for iss in ["A", "B", "C"]: + _kj = build_keyjar(keym) +- _kj.import_jwks(_kj.export_jwks(private=True), iss) ++ _kj = store_under_other_id(_kj, "", iss, True) + del _kj[""] + KEYJARS[iss] = _kj + +diff --git a/tests/test_05_oauth2.py b/tests/test_05_oauth2.py +index fc187db..2cf892c 100644 +--- a/tests/test_05_oauth2.py ++++ b/tests/test_05_oauth2.py +@@ -8,6 +8,7 @@ from cryptojwt.key_jar import build_keyjar + + from idpyoidc import verified_claim_name + from idpyoidc.exception import MissingRequiredAttribute ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message import DecodeError + from idpyoidc.message import json_deserializer + from idpyoidc.message import json_serializer +@@ -44,10 +45,10 @@ keym = [ + ] + + KEYJAR = build_keyjar(keys) +-KEYJAR.import_jwks(KEYJAR.export_jwks(private=True), "issuer") ++KEYJAR = store_under_other_id(KEYJAR,"", "issuer", True) + + IKEYJAR = build_keyjar(keys) +-IKEYJAR.import_jwks(IKEYJAR.export_jwks(private=True), "issuer") ++IKEYJAR = store_under_other_id(IKEYJAR, "", "issuer", True) + del IKEYJAR[""] + + +diff --git a/tests/test_07_session.py b/tests/test_07_session.py +index 94cbebd..b380635 100644 +--- a/tests/test_07_session.py ++++ b/tests/test_07_session.py +@@ -10,6 +10,8 @@ from cryptojwt.key_jar import init_key_jar + + from idpyoidc.exception import MessageException + from idpyoidc.exception import NotForMe ++from idpyoidc.key_import import import_jwks_as_json ++from idpyoidc.key_import import import_jwks_from_file + from idpyoidc.message.oidc import Claims + from idpyoidc.message.oidc import ClaimsRequest + from idpyoidc.message.oidc import IdToken +@@ -64,8 +66,8 @@ ISS_KEY = init_key_jar( + issuer_id=ISS, + ) + +-ISS_KEY.import_jwks_as_json(open(full_path("pub_client.jwks")).read(), CLIENT_ID) +-CLI_KEY.import_jwks_as_json(open(full_path("pub_iss.jwks")).read(), ISS) ++ISS_KEY = import_jwks_from_file(ISS_KEY, full_path("pub_client.jwks"), CLIENT_ID) ++CLI_KEY = import_jwks_from_file(CLI_KEY, full_path("pub_iss.jwks"), ISS) + + + class TestEndSessionResponse(object): +diff --git a/tests/test_08_transform.py b/tests/test_08_transform.py +index 71c83d9..33a2e14 100644 +--- a/tests/test_08_transform.py ++++ b/tests/test_08_transform.py +@@ -4,12 +4,12 @@ import pytest + from cryptojwt.utils import importer + + from idpyoidc.client.claims.oidc import Claims as OIDC_Claims +-from idpyoidc.client.claims.transform import create_registration_request +-from idpyoidc.client.claims.transform import preferred_to_registered +-from idpyoidc.client.claims.transform import supported_to_preferred + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + from idpyoidc.message.oidc import ProviderConfigurationResponse + from idpyoidc.message.oidc import RegistrationRequest ++from idpyoidc.transform import create_registration_request ++from idpyoidc.transform import preferred_to_registered ++from idpyoidc.transform import supported_to_preferred + + + class TestTransform: +@@ -42,117 +42,114 @@ class TestTransform: + + def test_supported(self): + # These are all the available configuration parameters +- assert set(self.supported.keys()) == { +- "acr_values_supported", +- "application_type", +- "backchannel_logout_session_required", +- "backchannel_logout_supported", +- "backchannel_logout_uri", +- "callback_uris", +- "client_id", +- "client_name", +- "client_secret", +- "client_uri", +- "contacts", +- "default_max_age", +- "encrypt_id_token_supported", +- "encrypt_request_object_supported", +- "encrypt_userinfo_supported", +- "frontchannel_logout_session_required", +- "frontchannel_logout_supported", +- "frontchannel_logout_uri", +- "id_token_encryption_alg_values_supported", +- "id_token_encryption_enc_values_supported", +- "id_token_signing_alg_values_supported", +- "initiate_login_uri", +- "jwks", +- "jwks_uri", +- "logo_uri", +- "policy_uri", +- "post_logout_redirect_uris", +- "redirect_uris", +- "request_object_encryption_alg_values_supported", +- "request_object_encryption_enc_values_supported", +- "request_object_signing_alg_values_supported", +- "request_parameter", +- "request_parameter_supported", +- "request_uri_parameter_supported", +- "request_uris", +- "requests_dir", +- "require_auth_time", +- "response_modes_supported", +- "response_types_supported", +- "scopes_supported", +- "sector_identifier_uri", +- "subject_types_supported", +- # 'token_endpoint_auth_method', +- "token_endpoint_auth_methods_supported", +- "token_endpoint_auth_signing_alg_values_supported", +- "tos_uri", +- "userinfo_encryption_alg_values_supported", +- "userinfo_encryption_enc_values_supported", +- "userinfo_signing_alg_values_supported", +- } ++ assert set(self.supported.keys()) == {'acr_values_supported', ++ 'application_type', ++ 'backchannel_logout_session_required', ++ 'backchannel_logout_supported', ++ 'backchannel_logout_uri', ++ 'callback_uris', ++ 'client_id', ++ 'client_name', ++ 'client_secret', ++ 'client_uri', ++ 'code_challenge_methods_supported', ++ 'contacts', ++ 'default_max_age', ++ 'encrypt_id_token_supported', ++ 'encrypt_request_object_supported', ++ 'encrypt_userinfo_supported', ++ 'frontchannel_logout_session_required', ++ 'frontchannel_logout_supported', ++ 'frontchannel_logout_uri', ++ 'id_token_encryption_alg_values_supported', ++ 'id_token_encryption_enc_values_supported', ++ 'id_token_signing_alg_values_supported', ++ 'initiate_login_uri', ++ 'jwks', ++ 'jwks_uri', ++ 'logo_uri', ++ 'policy_uri', ++ 'post_logout_redirect_uris', ++ 'redirect_uris', ++ 'request_object_encryption_alg_values_supported', ++ 'request_object_encryption_enc_values_supported', ++ 'request_object_signing_alg_values_supported', ++ 'request_parameter', ++ 'request_parameter_supported', ++ 'request_uri_parameter_supported', ++ 'request_uris', ++ 'requests_dir', ++ 'require_auth_time', ++ 'response_modes_supported', ++ 'response_types_supported', ++ 'scopes_supported', ++ 'sector_identifier_uri', ++ 'subject_types_supported', ++ 'token_endpoint_auth_methods_supported', ++ 'token_endpoint_auth_signing_alg_values_supported', ++ 'tos_uri', ++ 'userinfo_encryption_alg_values_supported', ++ 'userinfo_encryption_enc_values_supported', ++ 'userinfo_signing_alg_values_supported'} + + def test_oidc_setup(self): + # This is OP specified stuff + assert set(ProviderConfigurationResponse.c_param.keys()).difference( + set(self.supported) + ) == { +- "authorization_endpoint", +- "check_session_iframe", +- "claim_types_supported", +- "claims_locales_supported", +- "claims_parameter_supported", +- "claims_supported", +- "display_values_supported", +- "end_session_endpoint", +- "error", +- "error_description", +- "error_uri", +- "grant_types_supported", +- "issuer", +- "op_policy_uri", +- "op_tos_uri", +- "registration_endpoint", +- "require_request_uri_registration", +- "service_documentation", +- "token_endpoint", +- "ui_locales_supported", +- "userinfo_endpoint", +- "code_challenge_methods_supported", +- } ++ "authorization_endpoint", ++ "check_session_iframe", ++ "claim_types_supported", ++ "claims_locales_supported", ++ "claims_parameter_supported", ++ "claims_supported", ++ "display_values_supported", ++ "end_session_endpoint", ++ "error", ++ "error_description", ++ "error_uri", ++ "grant_types_supported", ++ "issuer", ++ "op_policy_uri", ++ "op_tos_uri", ++ "registration_endpoint", ++ "require_request_uri_registration", ++ "service_documentation", ++ "token_endpoint", ++ "ui_locales_supported", ++ "userinfo_endpoint", ++ } + + # parameters that are not mapped against what the OP's provider info says + assert set(self.supported).difference( + set(ProviderConfigurationResponse.c_param.keys()) + ) == { +- "application_type", +- "backchannel_logout_uri", +- "callback_uris", +- "client_id", +- "client_name", +- "client_secret", +- "client_uri", +- "contacts", +- "default_max_age", +- "encrypt_id_token_supported", +- "encrypt_request_object_supported", +- "encrypt_userinfo_supported", +- "frontchannel_logout_uri", +- "initiate_login_uri", +- "jwks", +- "logo_uri", +- "policy_uri", +- "post_logout_redirect_uris", +- "redirect_uris", +- "request_parameter", +- "request_uris", +- "requests_dir", +- "require_auth_time", +- "sector_identifier_uri", +- "tos_uri", +- } ++ "application_type", ++ "backchannel_logout_uri", ++ "callback_uris", ++ "client_id", ++ "client_name", ++ "client_secret", ++ "client_uri", ++ "contacts", ++ "default_max_age", ++ "encrypt_id_token_supported", ++ "encrypt_request_object_supported", ++ "encrypt_userinfo_supported", ++ "frontchannel_logout_uri", ++ "initiate_login_uri", ++ "jwks", ++ "logo_uri", ++ "policy_uri", ++ "post_logout_redirect_uris", ++ "redirect_uris", ++ "request_parameter", ++ "request_uris", ++ "requests_dir", ++ "require_auth_time", ++ "sector_identifier_uri", ++ "tos_uri", ++ } + + claims = OIDC_Claims() + # No input from the IDP so info is absent +@@ -173,6 +170,7 @@ class TestTransform: + "request_object_encryption_alg_values_supported", + "request_object_encryption_enc_values_supported", + "request_object_signing_alg_values_supported", ++ "request_parameter_supported", + "response_modes_supported", + "response_types_supported", + "scopes_supported", +@@ -245,27 +243,24 @@ class TestTransform: + ) + + # These are the claims that has default values +- assert set(claims.prefer.keys()) == { +- "application_type", +- "default_max_age", +- "encrypt_request_object_supported", +- "encrypt_userinfo_supported", +- "id_token_encryption_alg_values_supported", +- "id_token_encryption_enc_values_supported", +- "id_token_signing_alg_values_supported", +- "request_object_encryption_alg_values_supported", +- "request_object_encryption_enc_values_supported", +- "request_object_signing_alg_values_supported", +- "response_modes_supported", +- "response_types_supported", +- "scopes_supported", +- "subject_types_supported", +- "token_endpoint_auth_methods_supported", +- "token_endpoint_auth_signing_alg_values_supported", +- "userinfo_encryption_alg_values_supported", +- "userinfo_encryption_enc_values_supported", +- "userinfo_signing_alg_values_supported", +- } ++ assert set(claims.prefer.keys()) == {'application_type', ++ 'default_max_age', ++ 'id_token_encryption_alg_values_supported', ++ 'id_token_encryption_enc_values_supported', ++ 'id_token_signing_alg_values_supported', ++ 'request_object_encryption_alg_values_supported', ++ 'request_object_encryption_enc_values_supported', ++ 'request_object_signing_alg_values_supported', ++ 'request_parameter_supported', ++ 'response_modes_supported', ++ 'response_types_supported', ++ 'scopes_supported', ++ 'subject_types_supported', ++ 'token_endpoint_auth_methods_supported', ++ 'token_endpoint_auth_signing_alg_values_supported', ++ 'userinfo_encryption_alg_values_supported', ++ 'userinfo_encryption_enc_values_supported', ++ 'userinfo_signing_alg_values_supported'} + + # least common denominator + # The RP supports less than the OP +@@ -362,10 +357,13 @@ class TestTransform2: + "client_name", + "contacts", + "default_max_age", ++ 'encrypt_request_object_supported', ++ 'encrypt_userinfo_supported', + "id_token_signed_response_alg", + "logo_uri", + "redirect_uris", + "request_object_signing_alg", ++ 'request_parameter_supported', + "response_types", + "response_modes", # non-standard + "subject_type", +@@ -402,29 +400,28 @@ class TestTransform2: + registration_response=registration_response, + ) + +- assert set(to_use.keys()) == { +- "application_type", +- "client_name", +- "contacts", +- "default_max_age", +- "encrypt_request_object_supported", +- "encrypt_userinfo_supported", +- "id_token_signed_response_alg", +- "jwks_uri", +- "logo_uri", +- "redirect_uris", +- "request_object_signing_alg", +- "request_uris", +- "response_types", +- "response_modes", # non-standard +- "scope", +- "sector_identifier_uri", +- "subject_type", +- "token_endpoint_auth_method", +- "token_endpoint_auth_signing_alg", +- "userinfo_encrypted_response_alg", +- "userinfo_encrypted_response_enc", +- "userinfo_signed_response_alg", +- } ++ assert set(to_use.keys()) == {'application_type', ++ 'client_name', ++ 'contacts', ++ 'default_max_age', ++ 'id_token_signed_response_alg', ++ 'jwks_uri', ++ 'logo_uri', ++ 'redirect_uris', ++ 'request_object_signing_alg', ++ 'request_uris', ++ 'encrypt_userinfo_supported', ++ 'request_parameter_supported', ++ 'encrypt_request_object_supported', ++ 'response_modes', ++ 'response_types', ++ 'scope', ++ 'sector_identifier_uri', ++ 'subject_type', ++ 'token_endpoint_auth_method', ++ 'token_endpoint_auth_signing_alg', ++ 'userinfo_encrypted_response_alg', ++ 'userinfo_encrypted_response_enc', ++ 'userinfo_signed_response_alg'} + + assert to_use["subject_type"] == "pairwise" +diff --git a/tests/test_09_work_condition.py b/tests/test_09_work_condition.py +index 957d857..34dd006 100644 +--- a/tests/test_09_work_condition.py ++++ b/tests/test_09_work_condition.py +@@ -4,9 +4,10 @@ import pytest as pytest + from cryptojwt.utils import importer + + from idpyoidc.client.claims.oidc import Claims +-from idpyoidc.client.claims.transform import create_registration_request +-from idpyoidc.client.claims.transform import preferred_to_registered +-from idpyoidc.client.claims.transform import supported_to_preferred ++from idpyoidc.message.oidc import RegistrationRequest ++from idpyoidc.transform import create_registration_request ++from idpyoidc.transform import preferred_to_registered ++from idpyoidc.transform import supported_to_preferred + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + + KEYSPEC = [ +@@ -168,14 +169,14 @@ class TestWorkEnvironment: + "acr_values_supported": ["mfa"], + } + +- pref = self.claims.prefer = supported_to_preferred( ++ self.claims.prefer = supported_to_preferred( + supported=self.supported, + preference=self.claims.prefer, + base_url="https://example.com", + info=provider_info_response, + ) + +- registration_request = create_registration_request(self.claims.prefer, self.supported) ++ registration_request = self.claims.get_client_metadata(metadata_schema=RegistrationRequest) + + assert set(registration_request.keys()) == { + "application_type", +@@ -230,8 +231,8 @@ class TestWorkEnvironment: + "client_secret", + "contacts", + "default_max_age", +- "encrypt_request_object_supported", +- "encrypt_userinfo_supported", ++ 'encrypt_userinfo_supported', ++ 'encrypt_request_object_supported', + "id_token_signed_response_alg", + "jwks", + "jwks_uri", +@@ -239,6 +240,7 @@ class TestWorkEnvironment: + "redirect_uris", + "request_object_signing_alg", + "request_uris", ++ 'request_parameter_supported', + "response_modes", + "response_types", + "scope", +@@ -313,7 +315,7 @@ class TestWorkEnvironment: + info=provider_info_response, + ) + +- registration_request = create_registration_request(self.claims.prefer, self.supported) ++ registration_request = self.claims.get_client_metadata(metadata_schema=RegistrationRequest) + + assert set(registration_request.keys()) == { + "application_type", +@@ -376,6 +378,7 @@ class TestWorkEnvironment: + "logo_uri", + "redirect_uris", + "request_object_signing_alg", ++ 'request_parameter_supported', + "request_uris", + "response_modes", + "response_types", +diff --git a/tests/test_11_impexp.py b/tests/test_11_impexp.py +index 3f525b1..14e1e4e 100644 +--- a/tests/test_11_impexp.py ++++ b/tests/test_11_impexp.py +@@ -94,3 +94,27 @@ def test_flush(): + assert len(b.bundles) == 2 + for kb in b.bundles: + assert isinstance(kb, KeyBundle) ++ ++ ++def test_dict(): ++ b = ImpExpTest() ++ b.string = "foo" ++ b.list = ["a", "b", "c"] ++ b.dict = {"a": 1, "b": 2} ++ b.message = { ++ "scope": "openid", ++ "redirect_uri": "https://example.com/cb", ++ "response_type": "code", ++ "client_id": "abcdefg", ++ } ++ ++ dump = b.dump() ++ ++ b.flush() ++ ++ b.load(dump) ++ ++ assert b.string == "foo" ++ assert b.list == ["a", "b", "c"] ++ assert b.dict == {"a": 1, "b": 2} ++ assert isinstance(b.message, AuthorizationRequest) +diff --git a/tests/test_14_read_only_list_file.py b/tests/test_14_read_only_list_file.py +index 2abdf9e..fd30b9d 100644 +--- a/tests/test_14_read_only_list_file.py ++++ b/tests/test_14_read_only_list_file.py +@@ -24,6 +24,4 @@ def test_read_only_list_file(): + fp.write(line + '\n') + + # sleep(2) +- # assert _read_only.is_changed(FILE_NAME) is True + assert set(_read_only) == {"one", "two", "three"} +- assert _read_only[-1] == "three" +\ No newline at end of file +diff --git a/tests/test_20_config.py b/tests/test_20_config.py +index ad737ec..3f6b27a 100644 +--- a/tests/test_20_config.py ++++ b/tests/test_20_config.py +@@ -172,14 +172,12 @@ def test_init_crypto_keys(): + "keys": { + "private_path": "private/cookie_jwks.json", + "key_defs": [ +- {"type": "OCT", "use": ["enc"], "kid": "enc"}, +- {"type": "OCT", "use": ["sig"], "kid": "sig"}, ++ {"type": "OCT", "use": ["enc"], "kid": "key", "bytes": 32}, + ], + "read_only": False, + } + } + _res = init_encrypter(_conf) + assert _res["conf"]["class"] == DEFAULT_CRYPTO +- assert set(_res["conf"]["kwargs"].keys()) == {"password", "salt"} +- assert "password" in _res["conf"]["kwargs"] +- assert "salt" in _res["conf"]["kwargs"] ++ assert set(_res["conf"]["kwargs"].keys()) == {"key", "salt"} ++ assert len(_res["conf"]["kwargs"]["salt"]) == 16 +diff --git a/tests/test_21_abfile_no_cache.py b/tests/test_21_abfile_no_cache.py +new file mode 100644 +index 0000000..cfe7e86 +--- /dev/null ++++ b/tests/test_21_abfile_no_cache.py +@@ -0,0 +1,116 @@ ++import os ++import shutil ++ ++import pytest ++ ++from idpyoidc.impexp import ImpExp ++from idpyoidc.storage.abfile_no_cache import AbstractFileSystemNoCache ++ ++BASEDIR = os.path.abspath(os.path.dirname(__file__)) ++ ++ ++def full_path(local_file): ++ return os.path.join(BASEDIR, local_file) ++ ++ ++CLIENT_1 = { ++ "client_secret": "hemligtkodord", ++ "redirect_uris": [["https://example.com/cb", ""]], ++ "client_salt": "salted", ++ "token_endpoint_auth_method": "client_secret_post", ++ "response_types": ["code", "token"], ++} ++ ++CLIENT_2 = { ++ "client_secret": "spraket", ++ "redirect_uris": [["https://app1.example.net/foo", ""], ["https://app2.example.net/bar", ""]], ++ "response_types": ["code"], ++} ++ ++ ++class ImpExpTest(ImpExp): ++ parameter = { ++ "string": "", ++ "list": [], ++ "dict": "DICT_TYPE", ++ } ++ ++ ++class TestAFS(object): ++ @pytest.fixture(autouse=True) ++ def setup(self): ++ filename = full_path("afs") ++ if os.path.isdir(filename): ++ shutil.rmtree(filename) ++ ++ def test_create_cdb(self): ++ abf = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") ++ ++ # add a client ++ ++ abf["client_1"] = CLIENT_1 ++ ++ assert list(abf.keys()) == ["client_1"] ++ ++ # add another one ++ ++ abf["client_2"] = CLIENT_2 ++ ++ assert set(abf.keys()) == {"client_1", "client_2"} ++ ++ def test_read_cdb(self): ++ abf = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") ++ # add a client ++ abf["client_1"] = CLIENT_1 ++ # add another one ++ abf["client_2"] = CLIENT_2 ++ ++ afs_2 = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") ++ assert set(afs_2.keys()) == {"client_1", "client_2"} ++ ++ def test_dump_load_afs(self): ++ b = ImpExpTest() ++ b.string = "foo" ++ b.list = ["a", "b", "c"] ++ b.dict = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") ++ ++ # add a client ++ b.dict["client_1"] = CLIENT_1 ++ # add another one ++ b.dict["client_2"] = CLIENT_2 ++ ++ dump = b.dump() ++ ++ b_copy = ImpExpTest().load(dump) ++ assert b_copy ++ assert isinstance(b_copy.dict, AbstractFileSystemNoCache) ++ assert set(b_copy.dict.keys()) == {"client_1", "client_2"} ++ ++ def test_dump_load_dict(self): ++ b = ImpExpTest() ++ b.string = "foo" ++ b.list = ["a", "b", "c"] ++ b.dict = {"a": 1, "b": 2, "c": 3} ++ ++ dump = b.dump() ++ ++ b_copy = ImpExpTest().load(dump) ++ assert b_copy ++ assert isinstance(b_copy.dict, dict) ++ ++ def test_get(self): ++ abf = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") ++ # add a client ++ abf["client_1"] = CLIENT_1 ++ # add another one ++ abf["client_2"] = CLIENT_2 ++ ++ val = abf["client_2"] ++ assert val == CLIENT_2 ++ ++ del abf["client_2"] ++ ++ assert set(abf.keys()) == {"client_1"} ++ ++ abf.clear() ++ assert set(abf.keys()) == set() +diff --git a/tests/test_client_02_entity.py b/tests/test_client_02_entity.py +index 369ef7f..0598146 100644 +--- a/tests/test_client_02_entity.py ++++ b/tests/test_client_02_entity.py +@@ -39,7 +39,7 @@ class TestEntity: + assert _srv is None + + def test_get_client_id(self): +- assert self.entity.get_service_context().get_preference("client_id") == "Number5" ++ assert self.entity.client_id == "Number5" + assert self.entity.get_attribute("client_id") == "Number5" + + def test_get_service_by_endpoint_name(self): +diff --git a/tests/test_client_02b_entity_metadata.py b/tests/test_client_02b_entity_metadata.py +index 8f122a5..16002e4 100644 +--- a/tests/test_client_02b_entity_metadata.py ++++ b/tests/test_client_02b_entity_metadata.py +@@ -74,6 +74,7 @@ def test_create_client(): + "redirect_uris", + "request_object_signing_alg_values_supported", + "request_parameter", ++ "request_parameter_supported", + "response_modes_supported", + "response_types_supported", + "scopes_supported", +@@ -95,7 +96,7 @@ def test_create_client(): + + _conf_args = list(_context.collect_usage().keys()) + assert _conf_args +- assert len(_conf_args) == 23 ++ assert len(_conf_args) == 24 + rr = set(RegistrationRequest.c_param.keys()) + # The ones that are not defined and will therefore not appear in a registration request + d = rr.difference(set(_conf_args)) +@@ -146,3 +147,28 @@ def test_create_client_jwks_uri(): + client_config["jwks_uri"] = "https://rp.example.com/jwks_uri.json" + client = Entity(config=client_config) + assert client.get_service_context().get_preference("jwks_uri") ++ ++ ++def test_metadata(): ++ client = Entity(config=CLIENT_CONFIG, client_type="oidc") ++ # With entity type ++ metadata = client.context.claims.get_client_metadata("openid_relying_party", ++ metadata_schema=RegistrationRequest) ++ assert set(metadata.keys()) == {"openid_relying_party"} ++ # Without entity type, no endpoints. Typical client ++ metadata = client.context.claims.get_client_metadata(metadata_schema=RegistrationRequest) ++ assert set(metadata.keys()) == {'application_type', ++ 'backchannel_logout_session_required', ++ 'backchannel_logout_uri', ++ 'contacts', ++ 'default_max_age', ++ 'grant_types', ++ 'id_token_signed_response_alg', ++ 'redirect_uris', ++ 'request_object_signing_alg', ++ 'response_modes', ++ 'response_types', ++ 'subject_type', ++ 'token_endpoint_auth_method', ++ 'token_endpoint_auth_signing_alg', ++ 'userinfo_signed_response_alg'} +diff --git a/tests/test_client_04_service.py b/tests/test_client_04_service.py +index 95e0934..2eec542 100644 +--- a/tests/test_client_04_service.py ++++ b/tests/test_client_04_service.py +@@ -62,6 +62,7 @@ class TestService: + "jwks", + "redirect_uris", + "request_object_signing_alg", ++ 'request_parameter_supported', + "response_modes", + "response_types", + "scope", +diff --git a/tests/test_client_05_util.py b/tests/test_client_05_util.py +index 3a22416..057c954 100644 +--- a/tests/test_client_05_util.py ++++ b/tests/test_client_05_util.py +@@ -7,6 +7,7 @@ from urllib.parse import urlsplit + import pytest + + from idpyoidc.client.exception import WrongContentType ++from idpyoidc.client.util import get_content_type + from idpyoidc.client.util import get_deserialization_method + from idpyoidc.client.util import get_http_body + from idpyoidc.client.util import get_http_url +@@ -139,28 +140,35 @@ def test_verify_header(): + + def test_get_deserialization_method_json(): + resp = FakeResponse("application/json") +- assert get_deserialization_method(resp) == "json" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "json" + + resp = FakeResponse("application/json; charset=utf-8") +- assert get_deserialization_method(resp) == "json" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "json" + + resp.headers["content-type"] = "application/jrd+json" +- assert get_deserialization_method(resp) == "json" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "json" + + + def test_get_deserialization_method_jwt(): + resp = FakeResponse("application/jwt") +- assert get_deserialization_method(resp) == "jwt" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "jwt" + + + def test_get_deserialization_method_urlencoded(): + resp = FakeResponse(URL_ENCODED) +- assert get_deserialization_method(resp) == "urlencoded" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "urlencoded" + + + def test_get_deserialization_method_text(): + resp = FakeResponse("text/html") +- assert get_deserialization_method(resp) == "" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "" + + resp = FakeResponse("text/plain") +- assert get_deserialization_method(resp) == "" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "" +diff --git a/tests/test_client_06_client_authn.py b/tests/test_client_06_client_authn.py +index 52fb95c..6a42ae5 100644 +--- a/tests/test_client_06_client_authn.py ++++ b/tests/test_client_06_client_authn.py +@@ -3,26 +3,28 @@ import os + + import pytest + from cryptojwt.exception import MissingKey +-from cryptojwt.jws.jws import JWS + from cryptojwt.jws.jws import factory ++from cryptojwt.jws.jws import JWS + from cryptojwt.jwt import JWT + from cryptojwt.key_bundle import KeyBundle +-from cryptojwt.key_jar import KeyJar + from cryptojwt.key_jar import init_key_jar ++from cryptojwt.key_jar import KeyJar + + from idpyoidc.claims import Claims ++from idpyoidc.client.client_auth import assertion_jwt + from idpyoidc.client.client_auth import AuthnFailure ++from idpyoidc.client.client_auth import bearer_auth + from idpyoidc.client.client_auth import BearerBody + from idpyoidc.client.client_auth import BearerHeader + from idpyoidc.client.client_auth import ClientSecretBasic + from idpyoidc.client.client_auth import ClientSecretJWT + from idpyoidc.client.client_auth import ClientSecretPost + from idpyoidc.client.client_auth import PrivateKeyJWT +-from idpyoidc.client.client_auth import assertion_jwt +-from idpyoidc.client.client_auth import bearer_auth + from idpyoidc.client.client_auth import valid_service_context + from idpyoidc.client.entity import Entity + from idpyoidc.defaults import JWT_BEARER ++from idpyoidc.key_import import add_kb ++from idpyoidc.key_import import import_jwks + from idpyoidc.message import Message + from idpyoidc.message.oauth2 import AccessTokenRequest + from idpyoidc.message.oauth2 import AccessTokenResponse +@@ -88,12 +90,13 @@ def test_quote(): + ) + + assert ( +- http_args["headers"]["Authorization"] == "Basic " +- "Nzk2ZDhmYWUtYTQyZi00ZTRmLWFiMjUtZDYyMDViNmQ0ZmEyOk1LRU0vQTdQa243SnVVMExBY3h5SFZLdndkY3pzdWdhUFUwQmllTGI0Q2JRQWdRait5cGNhbkZPQ2IwL0ZBNWg=" ++ http_args["headers"]["Authorization"] == "Basic " ++ "Nzk2ZDhmYWUtYTQyZi00ZTRmLWFiMjUtZDYyMDViNmQ0ZmEyOk1LRU0vQTdQa243SnVVMExBY3h5SFZLdndkY3pzdWdhUFUwQmllTGI0Q2JRQWdRait5cGNhbkZPQ2IwL0ZBNWg=" + ) + + + class TestClientSecretBasic(object): ++ + def test_construct(self, entity): + _service = entity.get_service("") + request = _service.construct( +@@ -127,6 +130,7 @@ class TestClientSecretBasic(object): + + + class TestBearerHeader(object): ++ + def test_construct(self, entity): + request = ResourceRequest(access_token="Sesame") + bh = BearerHeader() +@@ -196,6 +200,7 @@ class TestBearerHeader(object): + + + class TestBearerBody(object): ++ + def test_construct(self, entity): + _token_service = entity.get_service("") + request = ResourceRequest(access_token="Sesame") +@@ -252,6 +257,7 @@ class TestBearerBody(object): + + + class TestClientSecretPost(object): ++ + def test_construct(self, entity): + _token_service = entity.get_service("") + request = _token_service.construct( +@@ -292,6 +298,7 @@ class TestClientSecretPost(object): + + + class TestPrivateKeyJWT(object): ++ + def test_construct(self, entity): + token_service = entity.get_service("") + kb_rsa = KeyBundle( +@@ -320,8 +327,8 @@ class TestPrivateKeyJWT(object): + + # Receiver + _kj = KeyJar() +- _kj.import_jwks(_keyjar.export_jwks(), issuer_id=_context.get_client_id()) +- _kj.add_kb(_context.get_client_id(), kb_rsa) ++ _kj = import_jwks(_kj, _keyjar.export_jwks(), _context.get_client_id()) ++ _kj = add_kb(_kj, kb_rsa, _context.get_client_id()) + jso = JWT(key_jar=_kj).unpack(cas) + assert _eq(jso.keys(), ["aud", "iss", "sub", "jti", "exp", "iat"]) + # assert _jwt.headers == {'alg': 'RS256'} +@@ -350,6 +357,7 @@ class TestPrivateKeyJWT(object): + + + class TestClientSecretJWT_TE(object): ++ + def test_client_secret_jwt(self, entity): + _service_context = entity.get_context() + _service_context.token_endpoint = "https://example.com/token" +@@ -487,6 +495,7 @@ class TestClientSecretJWT_TE(object): + + + class TestClientSecretJWT_UI(object): ++ + def test_client_secret_jwt(self, entity): + access_token_service = entity.get_service("") + +@@ -526,6 +535,7 @@ class TestClientSecretJWT_UI(object): + + + class TestValidClientInfo(object): ++ + def test_valid_service_context(self, entity): + _service_context = entity.get_context() + +diff --git a/tests/test_client_16_util.py b/tests/test_client_16_util.py +index a09d65a..57c4bf6 100644 +--- a/tests/test_client_16_util.py ++++ b/tests/test_client_16_util.py +@@ -12,6 +12,7 @@ from idpyoidc.client import util + from idpyoidc.client.exception import WrongContentType + from idpyoidc.client.util import JSON_ENCODED + from idpyoidc.client.util import URL_ENCODED ++from idpyoidc.client.util import get_content_type + from idpyoidc.client.util import get_deserialization_method + from idpyoidc.message.oauth2 import AccessTokenRequest + from idpyoidc.message.oauth2 import AuthorizationRequest +@@ -145,31 +146,38 @@ def test_verify_header(): + + def test_get_deserialization_method_json(): + resp = FakeResponse("application/json") +- assert get_deserialization_method(resp) == "json" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "json" + + resp = FakeResponse("application/json; charset=utf-8") +- assert get_deserialization_method(resp) == "json" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "json" + + resp.headers["content-type"] = "application/jrd+json" +- assert get_deserialization_method(resp) == "json" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "json" + + + def test_get_deserialization_method_jwt(): + resp = FakeResponse("application/jwt") +- assert get_deserialization_method(resp) == "jwt" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "jwt" + + + def test_get_deserialization_method_urlencoded(): + resp = FakeResponse(URL_ENCODED) +- assert get_deserialization_method(resp) == "urlencoded" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "urlencoded" + + + def test_get_deserialization_method_text(): + resp = FakeResponse("text/html") +- assert get_deserialization_method(resp) == "" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "" + + resp = FakeResponse("text/plain") +- assert get_deserialization_method(resp) == "" ++ ctype = get_content_type(resp) ++ assert get_deserialization_method(ctype) == "" + + + def test_verify_no_content_type(): +diff --git a/tests/test_client_20_oauth2.py b/tests/test_client_20_oauth2.py +index 5e5df3f..336ba13 100644 +--- a/tests/test_client_20_oauth2.py ++++ b/tests/test_client_20_oauth2.py +@@ -191,6 +191,6 @@ class TestClient2(object): + + def test_keyjar(self): + _keyjar = self.client.get_attribute("keyjar") +- assert len(_keyjar) == 2 # one issuer +- assert len(_keyjar[""]) == 3 +- assert len(_keyjar.get("sig")) == 3 ++ assert len(_keyjar) == 1 # ++ assert len(_keyjar[""]) == 2 ++ assert len(_keyjar.get("sig")) == 2 +diff --git a/tests/test_client_21_oidc_service.py b/tests/test_client_21_oidc_service.py +index 5eba310..ca1dfb6 100644 +--- a/tests/test_client_21_oidc_service.py ++++ b/tests/test_client_21_oidc_service.py +@@ -1,19 +1,22 @@ + import os + ++import pytest ++import responses + from cryptojwt.exception import UnsupportedAlgorithm + from cryptojwt.jws import jws + from cryptojwt.jws.utils import left_hash + from cryptojwt.jwt import JWT + from cryptojwt.key_jar import build_keyjar + from cryptojwt.key_jar import init_key_jar +-import pytest +-import responses + + from idpyoidc.client.defaults import DEFAULT_OIDC_SERVICES + from idpyoidc.client.entity import Entity + from idpyoidc.client.exception import ParameterError + from idpyoidc.client.oidc.registration import response_types_to_grant_types + from idpyoidc.exception import MissingRequiredAttribute ++from idpyoidc.key_import import import_jwks ++from idpyoidc.key_import import import_jwks_from_file ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AccessTokenResponse + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB +@@ -30,6 +33,7 @@ from idpyoidc.message.oidc.session import EndSessionRequest + + + class Response(object): ++ + def __init__(self, status_code, text, headers=None): + self.status_code = status_code + self.text = text +@@ -45,15 +49,19 @@ _dirname = os.path.dirname(os.path.abspath(__file__)) + + ISS = "https://example.com" + +-ISS_KEY = init_key_jar( +- public_path="{}/pub_iss.jwks".format(_dirname), +- private_path="{}/priv_iss.jwks".format(_dirname), +- key_defs=KEYSPEC, +- issuer_id=ISS, +- read_only=False, +-) +- +-ISS_KEY.import_jwks_as_json(open("{}/pub_client.jwks".format(_dirname)).read(), "client_id") ++# Issuers keys ++def issuers_keyjar(): ++ _keyjar = init_key_jar( ++ public_path="{}/pub_iss.jwks".format(_dirname), ++ private_path="{}/priv_iss.jwks".format(_dirname), ++ key_defs=KEYSPEC, ++ issuer_id=ISS, ++ read_only=False, ++ ) ++ ++ # add clients keys ++ _keyjar = import_jwks_from_file(_keyjar, f"{_dirname}/pub_client.jwks", "client_id") ++ return _keyjar + + + def make_keyjar(): +@@ -64,12 +72,12 @@ def make_keyjar(): + issuer_id="client_id", + read_only=False, + ) +- _keyjar.import_jwks(_keyjar.export_jwks(private=True, issuer_id="client_id"), issuer_id="") +- _keyjar.import_jwks_as_json(open("{}/pub_iss.jwks".format(_dirname)).read(), ISS) ++ _keyjar = store_under_other_id(_keyjar, "client_id", "", True) + return _keyjar + + + class TestAuthorization(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + client_config = { +@@ -94,6 +102,8 @@ class TestAuthorization(object): + _context.issuer = "https://example.com" + _context.map_supported_to_preferred() + _context.map_preferred_to_registered() ++ # Add the servers keys ++ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) + self.context = _context + self.service = entity.get_service("authorization") + +@@ -201,7 +211,7 @@ class TestAuthorization(object): + _jws = jws.factory(msg["request"]) + assert _jws + _resp = _jws.verify_compact( +- msg["request"], keys=ISS_KEY.get_signing_key(key_type="RSA", issuer_id="client_id") ++ msg["request"], keys=issuers_keyjar().get_signing_key(key_type="RSA", issuer_id="client_id") + ) + assert _resp + assert set(_resp.keys()) == { +@@ -214,6 +224,8 @@ class TestAuthorization(object): + "iss", + "aud", + "iat", ++ "jti", ++ "exp" + } + + def test_request_param(self): +@@ -245,7 +257,7 @@ class TestAuthorization(object): + self.service.endpoint = "https://example.com/authorize" + _info = self.service.get_request_parameters(request_args=req_args) + # Build an ID Token +- idt = JWT(key_jar=ISS_KEY, iss=ISS, lifetime=3600) ++ idt = JWT(key_jar=issuers_keyjar(), iss=ISS, lifetime=3600) + payload = {"sub": "123456789", "aud": ["client_id"], "nonce": "nonce"} + # have to calculate c_hash + alg = "RS256" +@@ -262,7 +274,7 @@ class TestAuthorization(object): + self.service.endpoint = "https://example.com/authorize" + _info = self.service.get_request_parameters(request_args=req_args) + # Build an ID Token +- idt = JWT(ISS_KEY, iss=ISS, lifetime=3600) ++ idt = JWT(issuers_keyjar(), iss=ISS, lifetime=3600) + payload = {"sub": "123456789", "aud": ["client_id"], "nonce": "noice"} + # have to calculate c_hash + alg = "RS256" +@@ -279,7 +291,7 @@ class TestAuthorization(object): + self.service.endpoint = "https://example.com/authorize" + self.service.get_request_parameters(request_args=req_args) + # Build an ID Token +- idt = JWT(ISS_KEY, iss=ISS, lifetime=3600) ++ idt = JWT(issuers_keyjar(), iss=ISS, lifetime=3600) + payload = {"sub": "123456789", "aud": ["client_id"]} + # have to calculate c_hash + alg = "RS256" +@@ -297,7 +309,7 @@ class TestAuthorization(object): + self.service.endpoint = "https://example.com/authorize" + self.service.get_request_parameters(request_args=req_args) + # Build an ID Token +- idt = JWT(ISS_KEY, iss=ISS, lifetime=3600, sign_alg="none") ++ idt = JWT(issuers_keyjar(), iss=ISS, lifetime=3600, sign_alg="none") + payload = {"sub": "123456789", "aud": ["client_id"], "nonce": req_args["nonce"]} + _idt = idt.pack(payload) + self.service.upstream_get("context").claims.set_usage( +@@ -312,6 +324,7 @@ class TestAuthorization(object): + + + class TestAuthorizationCallback(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + client_config = { +@@ -335,7 +348,7 @@ class TestAuthorizationCallback(object): + _context.issuer = "https://example.com" + _context.map_supported_to_preferred() + _context.map_preferred_to_registered() +- ++ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) + self.service = entity.get_service("authorization") + + def test_construct_code(self): +@@ -397,6 +410,7 @@ class TestAuthorizationCallback(object): + + + class TestAccessTokenRequest(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + client_config = { +@@ -410,6 +424,7 @@ class TestAccessTokenRequest(object): + _context.issuer = "https://example.com" + _context.provider_info = {"token_endpoint": f"{_context.issuer}/token"} + self.service = entity.get_service("accesstoken") ++ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) + + # add some history + auth_request = AuthorizationRequest( +@@ -475,6 +490,7 @@ class TestAccessTokenRequest(object): + + + class TestProviderInfo(object): ++ + @pytest.fixture(autouse=True) + def create_service(self): + self._iss = ISS +@@ -729,7 +745,8 @@ class TestProviderInfo(object): + # assert _context.claims.use == {} + resp = self.service.post_parse_response(provider_info_response) + +- iss_jwks = ISS_KEY.export_jwks_as_json(issuer_id=ISS) ++ iss_jwks = issuers_keyjar().export_jwks_as_json(issuer_id=ISS) ++ + with responses.RequestsMock() as rsps: + rsps.add("GET", resp["jwks_uri"], body=iss_jwks, status=200) + +@@ -739,9 +756,9 @@ class TestProviderInfo(object): + _context.map_preferred_to_registered() + + use_copy = self.service.upstream_get("context").claims.use.copy() +- # jwks content will change dynamically between runs +- assert "jwks" in use_copy +- del use_copy["jwks"] ++ if "jwks" in use_copy: ++ assert True ++ del use_copy["jwks"] + del use_copy["callback_uris"] + + assert use_copy == { +@@ -760,6 +777,7 @@ class TestProviderInfo(object): + "post_logout_redirect_uris": ["https://rp.example.com/post"], + "redirect_uris": ["https://example.com/cli/authz_cb"], + "request_object_signing_alg": "ES256", ++ 'request_parameter_supported': True, + "response_modes": ["query", "fragment", "form_post"], + "response_types": ["code"], + "scope": ["openid"], +@@ -807,6 +825,7 @@ class TestProviderInfo(object): + 'post_logout_redirect_uris', + 'redirect_uris', + 'request_object_signing_alg', ++ 'request_parameter_supported', + 'response_modes', + 'response_types', + 'scope', +@@ -816,7 +835,7 @@ class TestProviderInfo(object): + 'userinfo_signed_response_alg'} + resp = self.service.post_parse_response(provider_info_response) + +- iss_jwks = ISS_KEY.export_jwks_as_json(issuer_id=ISS) ++ iss_jwks = issuers_keyjar().export_jwks_as_json(issuer_id=ISS) + with responses.RequestsMock() as rsps: + rsps.add("GET", resp["jwks_uri"], body=iss_jwks, status=200) + +@@ -847,6 +866,7 @@ class TestProviderInfo(object): + "post_logout_redirect_uris": ["https://rp.example.com/post"], + "redirect_uris": ["https://example.com/cli/authz_cb"], + "request_object_signing_alg": "ES256", ++ 'request_parameter_supported': True, + "response_modes": ["query", "fragment", "form_post"], + "response_types": ["code"], + "scope": ["openid"], +@@ -872,11 +892,12 @@ def create_jws(val): + idts = IdToken(**val) + + return idts.to_jwt( +- key=ISS_KEY.get_signing_key("ec", issuer_id=ISS), algorithm="ES256", lifetime=lifetime ++ key=issuers_keyjar().get_signing_key("ec", issuer_id=ISS), algorithm="ES256", lifetime=lifetime + ) + + + class TestRegistration(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + self._iss = ISS +@@ -892,9 +913,11 @@ class TestRegistration(object): + services=DEFAULT_OIDC_SERVICES, + client_type="oidc", + ) +- entity.get_context().issuer = "https://example.com" +- entity.get_context().map_supported_to_preferred() ++ _context = entity.get_context() ++ _context.issuer = "https://example.com" ++ _context.map_supported_to_preferred() + self.service = entity.get_service("registration") ++ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) + + def test_construct(self): + _req = self.service.construct() +@@ -1040,6 +1063,7 @@ def test_config_logout_uri(): + + + class TestUserInfo(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + self._iss = ISS +@@ -1060,6 +1084,9 @@ class TestUserInfo(object): + entity.get_context().issuer = "https://example.com" + self.service = entity.get_service("userinfo") + ++ _context = entity.get_context() ++ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) ++ + entity.get_context().claims.use = { + "userinfo_signed_response_alg": "RS256", + "userinfo_encrypted_response_alg": "RSA-OAEP", +@@ -1074,7 +1101,9 @@ class TestUserInfo(object): + idtval = {"nonce": "KUEYfRM2VzKDaaKD", "sub": "diana", "iss": ISS, "aud": "client_id"} + idt = create_jws(idtval) + +- ver_idt = IdToken().from_jwt(idt, make_keyjar()) ++ _keyjar = make_keyjar() ++ _keyjar = import_jwks_from_file(_keyjar, f"{_dirname}/pub_iss.jwks", ISS) ++ ver_idt = IdToken().from_jwt(idt, _keyjar) + + token_response = AccessTokenResponse( + access_token="access_token", id_token=idt, __verified_id_token=ver_idt +@@ -1104,7 +1133,7 @@ class TestUserInfo(object): + "phone_number": "+1 (555) 123-4567", + } + +- srv = JWT(ISS_KEY, iss=ISS, sign_alg="ES256") ++ srv = JWT(issuers_keyjar(), iss=ISS, sign_alg="ES256") + _jwt = srv.pack(payload=claims) + + resp = OpenIDSchema( +@@ -1157,7 +1186,7 @@ class TestUserInfo(object): + + def test_unpack_signed_response(self): + resp = OpenIDSchema(sub="diana", given_name="Diana", family_name="krall", iss=ISS) +- sk = ISS_KEY.get_signing_key("rsa", issuer_id=ISS) ++ sk = issuers_keyjar().get_signing_key("rsa", issuer_id=ISS) + alg = self.service.upstream_get("context").get_sign_alg("userinfo") + _resp = self.service.parse_response( + resp.to_jwt(sk, algorithm=alg), state="abcde", sformat="jwt" +@@ -1168,16 +1197,18 @@ class TestUserInfo(object): + # Add encryption key + _kj = build_keyjar([{"type": "RSA", "use": ["enc"]}], issuer_id="") + # Own key jar gets the private key +- self.service.upstream_get("attribute", "keyjar").import_jwks( +- _kj.export_jwks(private=True), issuer_id="client_id" +- ) +- # opponent gets the public key +- ISS_KEY.import_jwks(_kj.export_jwks(), issuer_id="client_id") ++ _keyjar = self.service.upstream_get("attribute", "keyjar") ++ _keyjar = import_jwks(_keyjar, ++ _kj.export_jwks(private=True), ++ "client_id") ++ # opponent gets the client public keys ++ _keyjar = issuers_keyjar() ++ _keyjar = import_jwks(_keyjar, _kj.export_jwks(), "client_id") + + resp = OpenIDSchema( + sub="diana", given_name="Diana", family_name="krall", iss=ISS, aud="client_id" + ) +- enckey = ISS_KEY.get_encrypt_key("rsa", issuer_id="client_id") ++ enckey = _keyjar.get_encrypt_key("rsa", issuer_id="client_id") + algspec = self.service.upstream_get("context").get_enc_alg_enc(self.service.service_name) + + enc_resp = resp.to_jwe(enckey, **algspec) +@@ -1186,6 +1217,7 @@ class TestUserInfo(object): + + + class TestCheckSession(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + self._iss = ISS +@@ -1213,6 +1245,7 @@ class TestCheckSession(object): + + + class TestCheckID(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + self._iss = ISS +@@ -1240,6 +1273,7 @@ class TestCheckID(object): + + + class TestEndSession(object): ++ + @pytest.fixture(autouse=True) + def create_request(self): + self._iss = ISS +diff --git a/tests/test_client_24_oic_utils.py b/tests/test_client_24_oic_utils.py +index 4e79980..e7d177b 100644 +--- a/tests/test_client_24_oic_utils.py ++++ b/tests/test_client_24_oic_utils.py +@@ -1,8 +1,8 @@ + from cryptojwt.jwe.jwe import factory + from cryptojwt.key_jar import build_keyjar + +-from idpyoidc.client.oidc.utils import construct_request_uri +-from idpyoidc.client.oidc.utils import request_object_encryption ++from idpyoidc.client.request_object import construct_request_uri ++from idpyoidc.client.request_object import request_object_encryption + from idpyoidc.client.service_context import ServiceContext + from idpyoidc.message.oidc import AuthorizationRequest + +diff --git a/tests/test_client_27_conversation.py b/tests/test_client_27_conversation.py +index fbb2239..a280400 100644 +--- a/tests/test_client_27_conversation.py ++++ b/tests/test_client_27_conversation.py +@@ -9,10 +9,11 @@ from cryptojwt.key_jar import KeyJar + + from idpyoidc.client.entity import Entity + from idpyoidc.client.oidc.webfinger import WebFinger +-from idpyoidc.message.oidc import JRD ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oidc import AccessTokenResponse + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + from idpyoidc.message.oidc import AuthorizationResponse ++from idpyoidc.message.oidc import JRD + from idpyoidc.message.oidc import Link + from idpyoidc.message.oidc import OpenIDSchema + from idpyoidc.message.oidc import ProviderConfigurationResponse +@@ -28,20 +29,20 @@ JWKS_OP = { + "keys": [ + { + "d": "mcAW1xeNsjzyV1M7F7_cUHz0MIR" +- "-tcnKFJnbbo5UXxMRUPu17qwRHr8ttep1Ie64r2L9QlphcT9BjYd0KQ8ll3flIzLtiJv__MNPQVjk5bsYzb_erQRzSwLJU-aCcNFB8dIyQECzu-p44UVEPQUGzykImsSShvMQhcvrKiqqg7NlijJuEKHaKynV9voPsjwKYSqk6lH8kMloCaVS-dOkK-r7bZtbODUxx9GJWnxhX0JWXcdrPZRb29y9cdthrMcEaCXG23AxnMEfp-enDqarLHYTQrCBJXs_b-9k2d8v9zLm7E-Pf-0YGmaoJtX89lwQkO_SmFF3sXsnI2cFreqU3Q", ++ "-tcnKFJnbbo5UXxMRUPu17qwRHr8ttep1Ie64r2L9QlphcT9BjYd0KQ8ll3flIzLtiJv__MNPQVjk5bsYzb_erQRzSwLJU-aCcNFB8dIyQECzu-p44UVEPQUGzykImsSShvMQhcvrKiqqg7NlijJuEKHaKynV9voPsjwKYSqk6lH8kMloCaVS-dOkK-r7bZtbODUxx9GJWnxhX0JWXcdrPZRb29y9cdthrMcEaCXG23AxnMEfp-enDqarLHYTQrCBJXs_b-9k2d8v9zLm7E-Pf-0YGmaoJtX89lwQkO_SmFF3sXsnI2cFreqU3Q", + "e": "AQAB", + "kid": "c19uYlBJXzVfNjNZeGVnYmxncHZwUzZTZDVwUFdxdVJLU3AxQXdwaFdfbw", + "kty": "RSA", + "n": "3ZblhNL2CjRktLM9vyDn8jnA4G1B1HCpPh" +- "-gv2AK4m9qDBZPYZGOGqzeW3vanvLTBlqnPm0GHg4rOrfMEwwLrfMcgmg1y4GD0vVU8G9HP1" +- "-oUPtKUqaKOp313tFKzFh9_OHGQ6EmhxG7gegPR9kQXduTDXqBFi81MzRplIQ8DHLM3-n2CyDW1V" +- "-dhRVh" +- "-AM0ZcJyzR_DvZ3mhG44DysPdHQOSeWnpdn1d81" +- "-PriqZfhAF9tn1ihgtjXd5swf1HTSjLd7xv1hitGf2245Xmr" +- "-V2pQFzeMukLM3JKbTYbElsB7Zm0wZx49hZMtgx35XMoO04bifdbO3yLtTA5ovXN3fQ", ++ "-gv2AK4m9qDBZPYZGOGqzeW3vanvLTBlqnPm0GHg4rOrfMEwwLrfMcgmg1y4GD0vVU8G9HP1" ++ "-oUPtKUqaKOp313tFKzFh9_OHGQ6EmhxG7gegPR9kQXduTDXqBFi81MzRplIQ8DHLM3-n2CyDW1V" ++ "-dhRVh" ++ "-AM0ZcJyzR_DvZ3mhG44DysPdHQOSeWnpdn1d81" ++ "-PriqZfhAF9tn1ihgtjXd5swf1HTSjLd7xv1hitGf2245Xmr" ++ "-V2pQFzeMukLM3JKbTYbElsB7Zm0wZx49hZMtgx35XMoO04bifdbO3yLtTA5ovXN3fQ", + "p": "88aNu59aBn0elksaVznzoVKkdbT5B4euhOIEqJoFvFbEocw9mC4k" +- "-yozIAQSV5FEakoSPOl8lrymCoM3Q1fVHfaM9Rbb9RCRlsV1JOeVVZOE05HUdz8zOIqLBDEGM_oQqDwF_kp" +- "-4nDTZ1-dtnGdTo4Cf7QRuApzE_dwVabUCTc", ++ "-yozIAQSV5FEakoSPOl8lrymCoM3Q1fVHfaM9Rbb9RCRlsV1JOeVVZOE05HUdz8zOIqLBDEGM_oQqDwF_kp" ++ "-4nDTZ1-dtnGdTo4Cf7QRuApzE_dwVabUCTc", + "q": "6LOHuM7H_0kDrMTwUEX7Aubzr792GoJ6EgTKIQY25SAFTZpYwuC3NnqlAdy8foIa3d7eGU2yICRbBG0S_ITcooDFrOa7nZ6enMUclMTxW8FwwvBXeIHo9cIsrKYtOThGplz43Cvl73MK5M58ZRmuhaNYa6Mk4PL4UokARfEiDus", + "use": "sig", + }, +@@ -58,7 +59,7 @@ JWKS_OP = { + } + + OP_KEYJAR = KeyJar() +-OP_KEYJAR.import_jwks(JWKS_OP, "") ++OP_KEYJAR = import_jwks(OP_KEYJAR, JWKS_OP, "") + OP_PUBLIC_JWKS = OP_KEYJAR.export_jwks() + OP_BASEURL = "https://example.org/op" + +@@ -70,13 +71,13 @@ RP_JWKS = { + "kid": "Mk0yN2w0N3BZLWtyOEpQWGFmNDZvQi1hbDl2azR3ai1WNElGdGZQSFd6MA", + "e": "AQAB", + "n": "yPrOADZtGoa9jxFCmDsJ1nAYmzgznUxCtUlb_ty33" +- "-AFNEqzW_pSLr5g6RQAPGsvVQqbsb9AB18QNgz" +- "-eG7cnvKIIR7JXWCuGv_Q9MwoRD0-zaYGRbRvFoTZokZMB6euBfMo6kijJ" +- "-gdKuSaxIE84X_Fcf1ESAKJ0EX6Cxdm8hKkBelGIDPMW5z7EHQ8OuLCQtTJnDvbjEOk9sKzkKqVj53XFs5vjd4WUhxS6xIDcWE-lTafUpm0BsobklLePidHxyAMGOunL_Pt3RCLZGlWeWOO9fZhLtydiDWiZlcNR0FQEX_mfV1kCOHHBFN1VKOY2pyJpjp9djdtHxPZ9fP35w", ++ "-AFNEqzW_pSLr5g6RQAPGsvVQqbsb9AB18QNgz" ++ "-eG7cnvKIIR7JXWCuGv_Q9MwoRD0-zaYGRbRvFoTZokZMB6euBfMo6kijJ" ++ "-gdKuSaxIE84X_Fcf1ESAKJ0EX6Cxdm8hKkBelGIDPMW5z7EHQ8OuLCQtTJnDvbjEOk9sKzkKqVj53XFs5vjd4WUhxS6xIDcWE-lTafUpm0BsobklLePidHxyAMGOunL_Pt3RCLZGlWeWOO9fZhLtydiDWiZlcNR0FQEX_mfV1kCOHHBFN1VKOY2pyJpjp9djdtHxPZ9fP35w", + "d": "aRBTqGDLYFaXuba4LYSPe_5Vnq8erFg1dzfGU9Fmfi5KCjAS2z5cv_reBnpiNTODJt3Izn7AJhpYCyl3zdWGl8EJ0OabNalY2txoi9A-LI4nyrHEDaRpfkgszVwaWtYZbxrShMc8I5x_wvCGx7sX7Hoy6YgQreRFzw8Fy86MDncpmcUwQTnXVUMLgioeYz5gW6rwXkqj_NVyuHPiheykJG026cXFNBWplCk4ET1bvf_6ZB9QmLwO16Pu2O-dtu1HHDOqI7y6-YgKIC6mcLrQrF9-FO7NkilcOB7zODNiYzhDBQ2YJAbcdn_3M_lkhaFwR-n4WB7vCM0vNqz7lEg6QQ", + "p": "_STNoJFkX9_uw8whytVmTrHP5K7vcZBIH9nuCTvj137lC48ZpR1UARx4qShxHLfK7DrufHd7TYnJkEMNUHFmdKvkaVQMY0_BsBSvCrUl10gzxsI08hg53L17E1Pe73iZp3f5nA4eB-1YB-km1Cc-Xs10OPWedJHf9brlCPDLAb8", + "q": "yz9T0rPEc0ZPjSi45gsYiQL2KJ3UsPHmLrgOHq0D4UvsB6UFtUtOWh7A1UpQdmBuHjIJz" +- "-Iq7VH4kzlI6VxoXhwE69oxBXr4I7fBudZRvlLuIJS9M2wvsTVouj0DBYSR6ZlAQHCCou89P2P6zQCEaqu7bWXNcpyTixbbvOU1w9k", ++ "-Iq7VH4kzlI6VxoXhwE69oxBXr4I7fBudZRvlLuIJS9M2wvsTVouj0DBYSR6ZlAQHCCou89P2P6zQCEaqu7bWXNcpyTixbbvOU1w9k", + }, + { + "kty": "EC", +@@ -91,12 +92,12 @@ RP_JWKS = { + } + + RP_KEYJAR = KeyJar() +-RP_KEYJAR.import_jwks(RP_JWKS, "") +-RP_KEYJAR.import_jwks(OP_PUBLIC_JWKS, OP_BASEURL) ++RP_KEYJAR = import_jwks(RP_KEYJAR, RP_JWKS, "") ++RP_KEYJAR = import_jwks(RP_KEYJAR, OP_PUBLIC_JWKS, OP_BASEURL) + RP_BASEURL = "https://example.com/rp" + + SERVICE_PUBLIC_JWKS = RP_KEYJAR.export_jwks("") +-OP_KEYJAR.import_jwks(SERVICE_PUBLIC_JWKS, RP_BASEURL) ++OP_KEYJAR = import_jwks(OP_KEYJAR, SERVICE_PUBLIC_JWKS, RP_BASEURL) + + # --------------------------------------------------- + +@@ -155,11 +156,11 @@ def test_conversation(): + info = webfinger_service.get_request_parameters(request_args={"resource": "foobar@example.org"}) + + assert ( +- info["url"] == "https://example.org/.well-known/webfinger?rel=http" +- "%3A%2F" +- "%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer" +- "&resource" +- "=acct%3Afoobar%40example.org" ++ info["url"] == "https://example.org/.well-known/webfinger?rel=http" ++ "%3A%2F" ++ "%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer" ++ "&resource" ++ "=acct%3Afoobar%40example.org" + ) + + webfinger_response = json.dumps( +diff --git a/tests/test_client_28_stand_alone.py b/tests/test_client_28_stand_alone.py +index d3c3a2a..0ea921a 100644 +--- a/tests/test_client_28_stand_alone.py ++++ b/tests/test_client_28_stand_alone.py +@@ -11,6 +11,7 @@ from idpyoidc.client.defaults import OIDCONF_PATTERN + from idpyoidc.client.exception import Unsupported + from idpyoidc.client.oauth2.stand_alone_client import StandAloneClient + from idpyoidc.exception import VerificationError ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oidc import AccessTokenResponse + from idpyoidc.message.oidc import AuthorizationResponse + from idpyoidc.message.oidc import IdToken +@@ -416,7 +417,7 @@ class TestPostAuthn(object): + idval = {"nonce": _nonce, "sub": subject, "iss": _iss, "aud": _aud} + + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(ISSUER_KEYS.export_jwks(issuer_id=ISSUER), ISSUER) ++ _keyjar = import_jwks(_keyjar, ISSUER_KEYS.export_jwks(issuer_id=ISSUER), ISSUER) + + idts = IdToken(**idval) + return idts.to_jwt( +diff --git a/tests/test_client_30_rp_handler_oidc.py b/tests/test_client_30_rp_handler_oidc.py +index 3a3d75f..f02aa8d 100644 +--- a/tests/test_client_30_rp_handler_oidc.py ++++ b/tests/test_client_30_rp_handler_oidc.py +@@ -4,12 +4,13 @@ from urllib.parse import parse_qs + from urllib.parse import urlparse + from urllib.parse import urlsplit + +-from cryptojwt.key_jar import init_key_jar + import pytest + import responses ++from cryptojwt.key_jar import init_key_jar + + from idpyoidc.client.entity import Entity + from idpyoidc.client.rp_handler import RPHandler ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oidc import AccessTokenResponse + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + from idpyoidc.message.oidc import AuthorizationResponse +@@ -217,6 +218,7 @@ def iss_id(iss): + + + class TestRPHandler(object): ++ + @pytest.fixture(autouse=True) + def rphandler_setup(self): + self.rph = RPHandler( +@@ -270,6 +272,7 @@ class TestRPHandler(object): + 'id_token_signing_alg_values_supported', + 'redirect_uris', + 'request_object_signing_alg_values_supported', ++ 'request_parameter_supported', + 'response_modes_supported', + 'response_types_supported', + 'scopes_supported', +@@ -279,13 +282,13 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + # The key jar should only contain a symmetric key that is the clients + # secret. 2 because one is marked for encryption and the other signing + # usage. + +- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} ++ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} + keys = _keyjar.get_issuer_keys("") + assert len(keys) == 3 + +@@ -329,9 +332,9 @@ class TestRPHandler(object): + assert _context.issuer == _github_id + + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + +- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} ++ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} + keys = _keyjar.get_issuer_keys("") + assert len(keys) == 3 + +@@ -347,7 +350,7 @@ class TestRPHandler(object): + cb = _context.get_preference("callback_uris") + + assert set(cb.keys()) == {"request_uris", "redirect_uris"} +- assert set(cb["redirect_uris"].keys()) == {"query", "fragment"} ++ assert set(cb["redirect_uris"].keys()) == {"query", "fragment", "form_post"} + _hash = _context.iss_hash + + assert cb["redirect_uris"]["query"] == [f"https://example.com/rp/authz_cb/{_hash}"] +@@ -449,7 +452,7 @@ class TestRPHandler(object): + _github_id = iss_id("github") + _context = client.get_context() + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + _nonce = _session["nonce"] + _iss = _session["iss"] +@@ -524,7 +527,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -571,7 +574,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -618,7 +621,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -697,6 +700,7 @@ def test_get_provider_specific_service(): + + + class TestRPHandlerTier2(object): ++ + @pytest.fixture(autouse=True) + def rphandler_setup(self): + self.rph = RPHandler(BASE_URL, CLIENT_CONFIG, keyjar=CLI_KEY) +@@ -712,7 +716,7 @@ class TestRPHandlerTier2(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -818,6 +822,7 @@ class TestRPHandlerTier2(object): + + + class MockResponse: ++ + def __init__(self, status_code, text, headers=None): + self.status_code = status_code + self.text = text +@@ -825,6 +830,7 @@ class MockResponse: + + + class MockOP(object): ++ + def __init__(self, issuer, keyjar=None): + self.keyjar = keyjar + self.issuer = issuer +@@ -913,6 +919,7 @@ def test_rphandler_request(): + + + class TestRPHandlerWithMockOP(object): ++ + @pytest.fixture(autouse=True) + def rphandler_setup(self): + self.issuer = "https://github.com/login/oauth/authorize" +@@ -956,7 +963,7 @@ class TestRPHandlerWithMockOP(object): + ) + _github_id = iss_id("github") + _keyjar = client.get_attribute("keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + with responses.RequestsMock() as rsps: + rsps.add( + "POST", +diff --git a/tests/test_client_30_rph_defaults.py b/tests/test_client_30_rph_defaults.py +index 177b772..7648572 100644 +--- a/tests/test_client_30_rph_defaults.py ++++ b/tests/test_client_30_rph_defaults.py +@@ -45,11 +45,11 @@ class TestRPHandler(object): + 'id_token_encryption_alg_values_supported', + 'id_token_encryption_enc_values_supported', + 'id_token_signing_alg_values_supported', +- 'jwks_uri', + 'redirect_uris', + 'request_object_encryption_alg_values_supported', + 'request_object_encryption_enc_values_supported', + 'request_object_signing_alg_values_supported', ++ 'request_parameter_supported', + 'response_modes_supported', + 'response_types_supported', + 'scopes_supported', +@@ -61,7 +61,7 @@ class TestRPHandler(object): + 'userinfo_signing_alg_values_supported'} + + _keyjar = client.get_attribute("keyjar") +- assert list(_keyjar.owners()) == ["", BASE_URL] ++ assert list(_keyjar.owners()) == [""] + keys = _keyjar.get_issuer_keys("") + assert len(keys) == 2 + +@@ -116,9 +116,9 @@ class TestRPHandler(object): + "encrypt_request_object_supported", + "grant_types", + "id_token_signed_response_alg", +- "jwks_uri", + "redirect_uris", + "request_object_signing_alg", ++ 'request_parameter_supported', + "response_modes", + "response_types", + "scope", +@@ -180,4 +180,5 @@ class TestRPHandler(object): + rsps.add("POST", request_uri, body=_jws, status=200) + self.rph.do_client_registration(client, ISS_ID) + +- assert "jwks_uri" in _context.get("registration_response") ++ assert "client_id" in _context.get("registration_response") ++ assert _context.client_id +diff --git a/tests/test_client_41_rp_handler_persistent.py b/tests/test_client_41_rp_handler_persistent.py +index 8edce03..d70dd1d 100644 +--- a/tests/test_client_41_rp_handler_persistent.py ++++ b/tests/test_client_41_rp_handler_persistent.py +@@ -6,6 +6,7 @@ import responses + from cryptojwt.key_jar import init_key_jar + + from idpyoidc.client.rp_handler import RPHandler ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oidc import AccessTokenResponse + from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + from idpyoidc.message.oidc import AuthorizationResponse +@@ -249,7 +250,7 @@ class TestRPHandler(object): + assert _context.get("issuer") == _github_id + + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} + keys = _keyjar.get_issuer_keys("") +@@ -291,7 +292,7 @@ class TestRPHandler(object): + assert query["client_id"] == ["eeeeeeeee"] + assert query["redirect_uri"] == ["https://example.com/rp/authz_cb/github"] + assert query["response_type"] == ["code"] +- assert query["scope"] == ["user public_repo openid"] ++ assert query["scope"] == ["openid"] + + def test_get_session_information(self): + rph_1 = RPHandler( +@@ -376,7 +377,7 @@ class TestRPHandler(object): + _github_id = iss_id("github") + _context = client.get_context() + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + _nonce = _session["nonce"] + _iss = _session["iss"] +@@ -457,7 +458,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -508,7 +509,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +@@ -559,7 +560,7 @@ class TestRPHandler(object): + + _github_id = iss_id("github") + _keyjar = _context.upstream_get("attribute", "keyjar") +- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) ++ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) + + idts = IdToken(**idval) + _signed_jwt = idts.to_jwt( +diff --git a/tests/test_client_55_token_exchange.py b/tests/test_client_55_token_exchange.py +index 108e867..6fbc4ca 100644 +--- a/tests/test_client_55_token_exchange.py ++++ b/tests/test_client_55_token_exchange.py +@@ -4,6 +4,7 @@ import pytest + from cryptojwt.key_jar import init_key_jar + + from idpyoidc.client.entity import Entity ++from idpyoidc.key_import import import_jwks_from_file + from idpyoidc.message import Message + from idpyoidc.message.oauth2 import AccessTokenResponse + from idpyoidc.message.oauth2 import AuthorizationResponse +@@ -27,7 +28,7 @@ ISS_KEY = init_key_jar( + read_only=False, + ) + +-ISS_KEY.import_jwks_as_json(open("{}/pub_client.jwks".format(_dirname)).read(), "client_id") ++ISS_KEY = import_jwks_from_file(ISS_KEY, f"{_dirname}/pub_client.jwks", "client_id") + + + def create_jws(val): +@@ -63,6 +64,9 @@ class TestUserInfo(object): + }, + ) + entity.get_context().issuer = "https://example.com" ++ _context = entity.get_context() ++ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) ++ + self.service = entity.get_service("token_exchange") + _cstate = self.service.upstream_get("context").cstate + # Add history +@@ -72,7 +76,7 @@ class TestUserInfo(object): + idtval = {"nonce": "KUEYfRM2VzKDaaKD", "sub": "diana", "iss": ISS, "aud": "client_id"} + idt = create_jws(idtval) + +- ver_idt = IdToken().from_jwt(idt, make_keyjar()) ++ ver_idt = IdToken().from_jwt(idt, _context.keyjar) + + token_response = AccessTokenResponse( + access_token="access_token", id_token=idt, __verified_id_token=ver_idt +diff --git a/tests/test_server_05_token_handler.py b/tests/test_server_05_token_handler.py +index 21d247d..721cae7 100644 +--- a/tests/test_server_05_token_handler.py ++++ b/tests/test_server_05_token_handler.py +@@ -1,11 +1,6 @@ +-import base64 +-import hashlib +-import hmac + import os +-import secrets + + import pytest +-from cryptojwt.jwe.fernet import FernetEncrypter + + from idpyoidc.encrypter import default_crypt_config + from idpyoidc.server import Server +@@ -39,6 +34,7 @@ def test_is_expired(): + + + class TestDefaultToken(object): ++ + @pytest.fixture(autouse=True) + def setup_token_handler(self): + password = "The longer the better. Is this close to enough ?" +@@ -78,6 +74,7 @@ class TestDefaultToken(object): + + + class TestTokenHandler(object): ++ + @pytest.fixture(autouse=True) + def setup_token_handler(self): + grant_expires_in = 600 +@@ -282,3 +279,87 @@ def test_file(jwks): + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + token_handler = server.context.session_manager.token_handler + assert token_handler ++ ++def test_token_handler_from_config_2(): ++ conf = { ++ "issuer": "https://example.com/op", ++ "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, ++ "endpoint": { ++ "endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}}, ++ }, ++ "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": { ++ "kwargs": { ++ "lifetime": 600, ++ "crypt_conf": { ++ "kwargs": { ++ "key": "0987654321abcdefghijklmnop...---", ++ "salt": "abcdefghijklmnop", ++ "iterations": 1 ++ } ++ } ++ } ++ }, ++ "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}, ++ } ++ }, ++ }, ++ }, ++ "session_params": SESSION_PARAMS, ++ } ++ ++ server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) ++ token_handler = server.context.session_manager.token_handler ++ assert token_handler ++ assert len(token_handler.handler) == 4 ++ assert set(token_handler.handler.keys()) == { ++ "authorization_code", ++ "access_token", ++ "refresh_token", ++ "id_token", ++ } ++ assert isinstance(token_handler.handler["authorization_code"], DefaultToken) ++ assert isinstance(token_handler.handler["access_token"], JWTToken) ++ assert isinstance(token_handler.handler["refresh_token"], JWTToken) ++ assert isinstance(token_handler.handler["id_token"], IDToken) ++ ++ assert token_handler.handler["authorization_code"].lifetime == 600 ++ ++ assert token_handler.handler["access_token"].alg == "ES256" ++ assert token_handler.handler["access_token"].kwargs == {"add_claims_by_scope": True} ++ assert token_handler.handler["access_token"].lifetime == 3600 ++ assert token_handler.handler["access_token"].def_aud == ["https://example.org/appl"] ++ ++ assert token_handler.handler["refresh_token"].alg == "ES256" ++ assert token_handler.handler["refresh_token"].kwargs == {} ++ assert token_handler.handler["refresh_token"].lifetime == 3600 ++ assert token_handler.handler["refresh_token"].def_aud == ["https://example.org/appl"] ++ ++ assert token_handler.handler["id_token"].lifetime == 300 ++ assert "base_claims" in token_handler.handler["id_token"].kwargs ++ +diff --git a/tests/test_server_08_id_token.py b/tests/test_server_08_id_token.py +index ecc72c6..5a9cb33 100644 +--- a/tests/test_server_08_id_token.py ++++ b/tests/test_server_08_id_token.py +@@ -6,6 +6,7 @@ from cryptojwt import JWT + from cryptojwt import KeyJar + from cryptojwt.jws.jws import factory + ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oidc import AuthorizationRequest + from idpyoidc.server import Server + from idpyoidc.server.authn_event import create_authn_event +@@ -160,6 +161,7 @@ USER_ID = "diana" + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_session_manager(self): + self.server = Server(conf) +@@ -467,7 +469,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) +@@ -501,7 +503,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + assert "nickname" in res +@@ -514,7 +516,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + +@@ -531,7 +533,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + +@@ -546,7 +548,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + assert "foobar" not in res +@@ -567,7 +569,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + assert "address" in res +@@ -586,7 +588,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + +@@ -605,7 +607,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + assert "address" in res +@@ -627,7 +629,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + assert "address" in res +@@ -645,7 +647,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + # User information, from scopes -> claims +@@ -668,7 +670,7 @@ class TestEndpoint(object): + + client_keyjar = KeyJar() + _jwks = self.server.keyjar.export_jwks() +- client_keyjar.import_jwks(_jwks, self.context.issuer) ++ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) + _jwt = JWT(key_jar=client_keyjar, iss="client_1") + res = _jwt.unpack(id_token.value) + # Email didn't match +diff --git a/tests/test_server_09_authn_context.py b/tests/test_server_09_authn_context.py +index 4b9a72e..31b6ac7 100644 +--- a/tests/test_server_09_authn_context.py ++++ b/tests/test_server_09_authn_context.py +@@ -4,6 +4,7 @@ import os + import pytest + from cryptojwt.jwk.hmac import SYMKey + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.server import Server + from idpyoidc.server.authn_event import AuthnEvent + from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD +@@ -164,7 +165,7 @@ class TestAuthnBrokerEC: + "code id_token token", + ], + } +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + + self.server = server + +diff --git a/tests/test_server_12_session_life.py b/tests/test_server_12_session_life.py +index 50de8c8..64e7c13 100644 +--- a/tests/test_server_12_session_life.py ++++ b/tests/test_server_12_session_life.py +@@ -3,6 +3,7 @@ import os + import pytest + from cryptojwt.key_jar import init_key_jar + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest + from idpyoidc.message.oidc import RefreshAccessTokenRequest +@@ -202,7 +203,7 @@ KEYDEFS = [ + ISSUER = "https://example.com/" + + KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) +-KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") ++KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) + RESPONSE_TYPES_SUPPORTED = [ + ["code"], + ["token"], +diff --git a/tests/test_server_16_endpoint.py b/tests/test_server_16_endpoint.py +index 5a3b59d..7c02399 100755 +--- a/tests/test_server_16_endpoint.py ++++ b/tests/test_server_16_endpoint.py +@@ -209,7 +209,7 @@ class TestEndpoint(object): + def test_do_response_placement_body(self): + self.endpoint.response_placement = "body" + info = self.endpoint.do_response(EXAMPLE_MSG) +- assert ("Content-type", "application/json; charset=utf-8") in info["http_headers"] ++ assert ("Content-type", "application/json") in info["http_headers"] + assert ( + info["response"] == '{"name": "Doe, Jane", "given_name": "Jane", "family_name": ' + '"Doe"}' +@@ -217,6 +217,7 @@ class TestEndpoint(object): + + def test_do_response_placement_url(self): + self.endpoint.response_placement = "url" ++ self.endpoint.response_format = "urlencoded" + info = self.endpoint.do_response(EXAMPLE_MSG, return_uri="https://example.org/cb") + assert ("Content-type", "application/x-www-form-urlencoded") in info["http_headers"] + assert ( +diff --git a/tests/test_server_16_endpoint_context.py b/tests/test_server_16_endpoint_context.py +index bf4b828..a462ce8 100644 +--- a/tests/test_server_16_endpoint_context.py ++++ b/tests/test_server_16_endpoint_context.py +@@ -1,17 +1,14 @@ + import copy + import os + +-import pytest + from cryptojwt.key_jar import build_keyjar ++import pytest + +-from idpyoidc import metadata ++from idpyoidc import alg_info + from idpyoidc.server import OPConfiguration + from idpyoidc.server import Server + from idpyoidc.server.endpoint import Endpoint +-from idpyoidc.server.exception import OidcEndpointError + from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD +-from idpyoidc.server.util import allow_refresh_token +- + from . import CRYPT_CONFIG + from . import SESSION_PARAMS + from . import full_path +@@ -28,9 +25,9 @@ class Endpoint_1(Endpoint): + name = "userinfo" + _supports = { + "claim_types_supported": ["normal", "aggregated", "distributed"], +- "userinfo_signing_alg_values_supported": metadata.get_signing_algs(), +- "userinfo_encryption_alg_values_supported": metadata.get_encryption_algs(), +- "userinfo_encryption_enc_values_supported": metadata.get_encryption_encs(), ++ "userinfo_signing_alg_values_supported": alg_info.get_signing_algs(), ++ "userinfo_encryption_alg_values_supported": alg_info.get_encryption_algs(), ++ "userinfo_encryption_enc_values_supported": alg_info.get_encryption_encs(), + "client_authn_method": ["bearer_header", "bearer_body"], + "encrypt_userinfo_supported": False, + } +diff --git a/tests/test_server_17_client_authn.py b/tests/test_server_17_client_authn.py +index b329644..aadaa72 100644 +--- a/tests/test_server_17_client_authn.py ++++ b/tests/test_server_17_client_authn.py +@@ -6,14 +6,16 @@ from unittest.mock import MagicMock + import pytest + from cryptojwt.jws.exception import NoSuitableSigningKeys + from cryptojwt.jwt import JWT +-from cryptojwt.key_jar import KeyJar + from cryptojwt.key_jar import build_keyjar ++from cryptojwt.key_jar import KeyJar + from cryptojwt.utils import as_bytes + from cryptojwt.utils import as_unicode + + from idpyoidc.defaults import JWT_BEARER +-from idpyoidc.server import Server ++from idpyoidc.key_import import import_jwks + from idpyoidc.server import do_endpoints ++from idpyoidc.server import Server ++from idpyoidc.server.client_authn import basic_authn + from idpyoidc.server.client_authn import BearerBody + from idpyoidc.server.client_authn import BearerHeader + from idpyoidc.server.client_authn import ClientSecretBasic +@@ -21,7 +23,6 @@ from idpyoidc.server.client_authn import ClientSecretJWT + from idpyoidc.server.client_authn import ClientSecretPost + from idpyoidc.server.client_authn import JWSAuthnMethod + from idpyoidc.server.client_authn import PrivateKeyJWT +-from idpyoidc.server.client_authn import basic_authn + from idpyoidc.server.client_authn import verify_client + from idpyoidc.server.endpoint import Endpoint + from idpyoidc.server.exception import ClientAuthenticationError +@@ -49,7 +50,7 @@ class Endpoint_3(Endpoint): + name = "endpoint_3" + + def __init__( +- self, upstream_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs ++ self, upstream_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs + ): + Endpoint.__init__( + self, +@@ -127,6 +128,7 @@ def get_client_id_from_token(context, token, request=None): + + + class TestClientSecretBasic: ++ + @pytest.fixture(autouse=True) + def setup(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -162,6 +164,7 @@ class TestClientSecretBasic: + + + class TestClientSecretPost: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -185,6 +188,7 @@ class TestClientSecretPost: + + + class TestClientSecretJWT: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -194,7 +198,7 @@ class TestClientSecretJWT: + + def test_client_secret_jwt(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has at this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -212,6 +216,7 @@ class TestClientSecretJWT: + + + class TestPrivateKeyJWT: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -225,10 +230,10 @@ class TestPrivateKeyJWT: + # Own dynamic keys + client_keyjar = build_keyjar(KEYDEFS) + # The servers keys +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwks = client_keyjar.export_jwks() +- self.server.keyjar.import_jwks(_jwks, client_id) ++ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") + _jwt.with_jti = True +@@ -246,10 +251,10 @@ class TestPrivateKeyJWT: + # Own dynamic keys + client_keyjar = build_keyjar(KEYDEFS) + # The servers keys +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwks = client_keyjar.export_jwks() +- self.server.keyjar.import_jwks(_jwks, client_id) ++ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") + _jwt.with_jti = True +@@ -273,10 +278,10 @@ class TestPrivateKeyJWT: + # Own dynamic keys + client_keyjar = build_keyjar(KEYDEFS) + # The servers keys +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwks = client_keyjar.export_jwks() +- self.server.keyjar.import_jwks(_jwks, client_id) ++ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") + _jwt.with_jti = True +@@ -295,6 +300,7 @@ class TestPrivateKeyJWT: + + + class TestBearerHeader: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -318,6 +324,7 @@ class TestBearerHeader: + + + class TestBearerBody: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -341,6 +348,7 @@ class TestBearerBody: + + + class TestJWSAuthnMethod: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -352,7 +360,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_wrong_key(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # Fake symmetric key + client_keyjar.add_symmetric("", "client_secret:client_secret", ["sig"]) + +@@ -366,7 +374,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_iss(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar,KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -381,7 +389,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_token_endpoint(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -401,7 +409,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_not_me(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has at this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -419,7 +427,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_userinfo_endpoint(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -465,6 +473,7 @@ def test_basic_auth_wrong_token(): + + + class TestVerify: ++ + @pytest.fixture(autouse=True) + def create_method(self): + self.server = Server(conf=CONF, keyjar=KEYJAR) +@@ -520,7 +529,7 @@ class TestVerify: + + def test_verify_client_jws_authn_method(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -582,6 +591,7 @@ class TestVerify: + + + class TestVerify2: ++ + @pytest.fixture(autouse=True) + def create_method(self): + self.server = Server(conf=CONF, keyjar=KEYJAR) +@@ -591,7 +601,7 @@ class TestVerify2: + + def test_verify_client_jws_authn_method(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +diff --git a/tests/test_server_20a_server.py b/tests/test_server_20a_server.py +index 8413b01..32968ed 100755 +--- a/tests/test_server_20a_server.py ++++ b/tests/test_server_20a_server.py +@@ -122,7 +122,7 @@ def test_capabilities_default(): + "id_token", + "code id_token", + } +- assert server.context.provider_info["request_uri_parameter_supported"] is True ++ assert server.context.provider_info["request_uri_parameter_supported"] is False + assert server.context.get_preference("jwks_uri") == "https://127.0.0.1:443/static/jwks.json" + + +@@ -137,10 +137,7 @@ def test_capabilities_subset2(): + _cnf = deepcopy(CONF) + _cnf["response_types_supported"] = ["code", "id_token"] + server = Server(_cnf) +- assert set(server.context.provider_info["response_types_supported"]) == { +- "code", +- "id_token", +- } ++ assert set(server.context.provider_info["response_types_supported"]) == {"code", "id_token"} + + + def test_capabilities_bool(): +diff --git a/tests/test_server_20d_client_authn.py b/tests/test_server_20d_client_authn.py +index d45e342..152cb52 100755 +--- a/tests/test_server_20d_client_authn.py ++++ b/tests/test_server_20d_client_authn.py +@@ -4,13 +4,17 @@ from unittest.mock import MagicMock + import pytest + from cryptojwt.jws.exception import NoSuitableSigningKeys + from cryptojwt.jwt import JWT +-from cryptojwt.key_jar import KeyJar + from cryptojwt.key_jar import build_keyjar ++from cryptojwt.key_jar import KeyJar + from cryptojwt.utils import as_bytes + from cryptojwt.utils import as_unicode + + from idpyoidc.defaults import JWT_BEARER ++from idpyoidc.key_import import add_keys ++from idpyoidc.key_import import add_symmetric ++from idpyoidc.key_import import import_jwks + from idpyoidc.server import Server ++from idpyoidc.server.client_authn import basic_authn + from idpyoidc.server.client_authn import BearerBody + from idpyoidc.server.client_authn import BearerHeader + from idpyoidc.server.client_authn import ClientSecretBasic +@@ -18,7 +22,6 @@ from idpyoidc.server.client_authn import ClientSecretJWT + from idpyoidc.server.client_authn import ClientSecretPost + from idpyoidc.server.client_authn import JWSAuthnMethod + from idpyoidc.server.client_authn import PrivateKeyJWT +-from idpyoidc.server.client_authn import basic_authn + from idpyoidc.server.client_authn import verify_client + from idpyoidc.server.exception import ClientAuthenticationError + from idpyoidc.server.exception import InvalidToken +@@ -88,6 +91,7 @@ def get_client_id_from_token(context, token, request=None): + + + class TestClientSecretBasic: ++ + @pytest.fixture(autouse=True) + def setup(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -122,6 +126,7 @@ class TestClientSecretBasic: + + + class TestClientSecretPost: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -145,6 +150,7 @@ class TestClientSecretPost: + + + class TestClientSecretJWT: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -154,9 +160,10 @@ class TestClientSecretJWT: + + def test_client_secret_jwt(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) +- # The only own key the client has a this point +- client_keyjar.add_symmetric("", client_secret, ["sig"]) ++ # The only own key the client has at this point ++ client_keyjar = add_symmetric(client_keyjar, client_secret, "") ++ # The issuers keys ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="HS256") + _jwt.with_jti = True +@@ -164,6 +171,7 @@ class TestClientSecretJWT: + + request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER} + ++ self.context.keyjar = add_keys(self.context.keyjar, client_keyjar.get(key_use="sig", key_type="oct"), client_id) + assert self.method.is_usable(request=request) + authn_info = self.method.verify(request=request) + +@@ -172,6 +180,7 @@ class TestClientSecretJWT: + + + class TestPrivateKeyJWT: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -184,10 +193,10 @@ class TestPrivateKeyJWT: + # Own dynamic keys + client_keyjar = build_keyjar(KEYDEFS) + # The servers keys +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwks = client_keyjar.export_jwks() +- self.server.keyjar.import_jwks(_jwks, client_id) ++ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") + _jwt.with_jti = True +@@ -205,10 +214,10 @@ class TestPrivateKeyJWT: + # Own dynamic keys + client_keyjar = build_keyjar(KEYDEFS) + # The servers keys +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwks = client_keyjar.export_jwks() +- self.server.keyjar.import_jwks(_jwks, client_id) ++ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") + _jwt.with_jti = True +@@ -232,10 +241,10 @@ class TestPrivateKeyJWT: + # Own dynamic keys + client_keyjar = build_keyjar(KEYDEFS) + # The servers keys +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + + _jwks = client_keyjar.export_jwks() +- self.server.keyjar.import_jwks(_jwks, client_id) ++ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) + + _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") + _jwt.with_jti = True +@@ -254,6 +263,7 @@ class TestPrivateKeyJWT: + + + class TestBearerHeader: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -276,6 +286,7 @@ class TestBearerHeader: + + + class TestBearerBody: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -298,6 +309,7 @@ class TestBearerBody: + + + class TestJWSAuthnMethod: ++ + @pytest.fixture(autouse=True) + def create_method(self): + server = Server(conf=CONF, keyjar=KEYJAR) +@@ -308,7 +320,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_wrong_key(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # Fake symmetric key + client_keyjar.add_symmetric("", "client_secret:client_secret", ["sig"]) + +@@ -322,7 +334,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_iss(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -337,7 +349,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_token_endpoint(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -357,7 +369,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_not_me(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has at this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -375,7 +387,7 @@ class TestJWSAuthnMethod: + + def test_jws_authn_method_aud_userinfo_endpoint(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -421,6 +433,7 @@ def test_basic_auth_wrong_token(): + + + class TestVerify: ++ + @pytest.fixture(autouse=True) + def create_method(self): + self.server = Server(conf=CONF, keyjar=KEYJAR) +@@ -475,7 +488,7 @@ class TestVerify: + + def test_verify_client_jws_authn_method(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +@@ -546,6 +559,7 @@ class TestVerify: + + + class TestVerify2: ++ + @pytest.fixture(autouse=True) + def create_method(self): + self.server = Server(conf=CONF, keyjar=KEYJAR) +@@ -554,7 +568,7 @@ class TestVerify2: + + def test_verify_client_jws_authn_method(self): + client_keyjar = KeyJar() +- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) ++ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) + # The only own key the client has a this point + client_keyjar.add_symmetric("", client_secret, ["sig"]) + +diff --git a/tests/test_server_20e_jwt_token.py b/tests/test_server_20e_jwt_token.py +index d824bd1..ef295bf 100644 +--- a/tests/test_server_20e_jwt_token.py ++++ b/tests/test_server_20e_jwt_token.py +@@ -4,6 +4,7 @@ import pytest + from cryptojwt.jwt import JWT + from cryptojwt.key_jar import init_key_jar + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest + from idpyoidc.server import Server +@@ -31,7 +32,7 @@ KEYDEFS = [ + ISSUER = "https://example.com/" + + KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) +-KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") ++KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) + + RESPONSE_TYPES_SUPPORTED = [ + ["code"], +@@ -98,6 +99,7 @@ def full_path(local_file): + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +@@ -297,6 +299,7 @@ class TestEndpoint(object): + + + class TestEndpointWebID(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + _scope2claims = SCOPE2CLAIMS.copy() +diff --git a/tests/test_server_22_oidc_provider_config_endpoint.py b/tests/test_server_22_oidc_provider_config_endpoint.py +index 7000d72..fe1f076 100755 +--- a/tests/test_server_22_oidc_provider_config_endpoint.py ++++ b/tests/test_server_22_oidc_provider_config_endpoint.py +@@ -19,12 +19,8 @@ KEYDEFS = [ + + RESPONSE_TYPES_SUPPORTED = [ + ["code"], +- ["token"], + ["id_token"], +- ["code", "token"], + ["code", "id_token"], +- ["id_token", "token"], +- ["code", "token", "id_token"], + ["none"], + ] + +@@ -92,16 +88,16 @@ class TestProviderConfigEndpoint(object): + assert _msg["token_endpoint"] == "https://example.com/token" + assert _msg["jwks_uri"] == "https://example.com/static/jwks.json" + assert "claims_supported" not in _msg # No default for this +- assert ("Content-type", "application/json; charset=utf-8") in msg["http_headers"] ++ assert ("Content-type", "application/json") in msg["http_headers"] + +- def test_scopes_supported(self, conf): +- scopes_supported = ["openid", "random", "profile"] +- conf["scopes_supported"] = scopes_supported +- +- server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) +- endpoint = server.get_endpoint("provider_config") +- args = endpoint.process_request() +- msg = endpoint.do_response(args["response_args"]) +- assert isinstance(msg, dict) +- _msg = json.loads(msg["response"]) +- assert set(_msg["scopes_supported"]) == set(scopes_supported) ++ # def test_scopes_supported(self, conf): ++ # scopes_supported = ["openid", "random", "profile"] ++ # conf["scopes_supported"] = scopes_supported ++ # ++ # server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) ++ # endpoint = server.get_endpoint("provider_config") ++ # args = endpoint.process_request() ++ # msg = endpoint.do_response(args["response_args"]) ++ # assert isinstance(msg, dict) ++ # _msg = json.loads(msg["response"]) ++ # assert set(_msg["scopes_supported"]) == set(scopes_supported) +diff --git a/tests/test_server_24_oauth2_authorization_endpoint.py b/tests/test_server_24_oauth2_authorization_endpoint.py +index 925424c..d0a010c 100755 +--- a/tests/test_server_24_oauth2_authorization_endpoint.py ++++ b/tests/test_server_24_oauth2_authorization_endpoint.py +@@ -5,16 +5,17 @@ from http.cookies import SimpleCookie + from urllib.parse import parse_qs + from urllib.parse import urlparse + +-from cryptojwt.jws.jws import factory + import pytest + import yaml + from cryptojwt import KeyJar ++from cryptojwt.jws.jws import factory + 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.key_import import store_under_other_id + from idpyoidc.message.oauth2 import AuthorizationErrorResponse + from idpyoidc.message.oauth2 import AuthorizationRequest + from idpyoidc.message.oauth2 import AuthorizationResponse +@@ -31,8 +32,8 @@ 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.authorization import FORM_POST + from idpyoidc.server.oauth2.authorization import get_uri + from idpyoidc.server.oauth2.authorization import inputs + from idpyoidc.server.oauth2.authorization import join_query +@@ -84,6 +85,7 @@ USERINFO_db = json.loads(open(full_path("users.json")).read()) + + + class SimpleCookieDealer(object): ++ + def __init__(self, name=""): + self.name = name + +@@ -159,6 +161,7 @@ clients: + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +@@ -265,7 +268,7 @@ class TestEndpoint(object): + context = server.context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = _clients["clients"] +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + self.context = context + self.endpoint = server.get_endpoint("authorization") + self.session_manager = context.session_manager +@@ -370,7 +373,8 @@ class TestEndpoint(object): + ) + def test_verify_uri_localhost_ipv4_native_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], ++ "application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") +@@ -381,15 +385,20 @@ class TestEndpoint(object): + ("http://[::1]:9999/auth_cb", "http://[::1]:3456/auth_cb"), + ("http://[::1]/auth_cb", "http://[::1]/auth_cb"), + ("http://[::1]/auth_cb", "http://[::1]:3456/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), + ] + ) + def test_verify_uri_localhost_ipv6_native_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], ++ "application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") +@@ -403,10 +412,11 @@ class TestEndpoint(object): + ) + def test_verify_uri_literal_localhost_native_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], ++ "application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + with pytest.raises(RedirectURIError): +- verify_uri(_context, request, "redirect_uri", "client_id") ++ verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ +@@ -417,71 +427,85 @@ class TestEndpoint(object): + ) + def test_verify_uri_localhost_ipv4_web_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_WEB} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], ++ "application_type": APPLICATION_TYPE_WEB} + request = {"redirect_uri": redirect_uri} + with pytest.raises(RedirectURIError): +- verify_uri(_context, request, "redirect_uri", "client_id") ++ verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ + ("http://[::1]:9999/auth_cb", "http://[::1]/auth_cb"), + ("http://[::1]:9999/auth_cb", "http://[::1]:3456/auth_cb"), + ("http://[::1]/auth_cb", "http://[::1]:3456/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), + ] + ) + def test_verify_uri_localhost_ipv6_web_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_WEB} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], ++ "application_type": APPLICATION_TYPE_WEB} + request = {"redirect_uri": redirect_uri} + with pytest.raises(RedirectURIError): +- verify_uri(_context, request, "redirect_uri", "client_id") ++ verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ +- ("http://127.0.0.1:9999/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:9999/auth_cb?foo=bar"), +- ("http://127.0.0.1:9999/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), +- ("http://127.0.0.1/auth_cb", {"foo":["bar"]}, "http://127.0.0.1/auth_cb?foo=bar"), +- ("http://127.0.0.1/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), ++ ("http://127.0.0.1:9999/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1:9999/auth_cb?foo=bar"), ++ ("http://127.0.0.1:9999/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), ++ ("http://127.0.0.1/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1/auth_cb?foo=bar"), ++ ("http://127.0.0.1/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), + ] + ) +- def test_verify_uri_qp_localhost_ipv4_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): ++ def test_verify_uri_qp_localhost_ipv4_native_client(self, client_redirect_uri, client_redirect_uri_qp, ++ redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)],"application_type": APPLICATION_TYPE_NATIVE} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], ++ "application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ +- ("http://[::1]:9999/auth_cb", {"foo":["bar"]}, "http://[::1]:9999/auth_cb?foo=bar"), +- ("http://[::1]:9999/auth_cb", {"foo":["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), +- ("http://[::1]/auth_cb", {"foo":["bar"]}, "http://[::1]/auth_cb?foo=bar"), +- ("http://[::1]/auth_cb", {"foo":["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), +- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), ++ ("http://[::1]:9999/auth_cb", {"foo": ["bar"]}, "http://[::1]:9999/auth_cb?foo=bar"), ++ ("http://[::1]:9999/auth_cb", {"foo": ["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), ++ ("http://[::1]/auth_cb", {"foo": ["bar"]}, "http://[::1]/auth_cb?foo=bar"), ++ ("http://[::1]/auth_cb", {"foo": ["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo": ["bar"]}, ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo": ["bar"]}, ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo": ["bar"]}, ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), ++ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo": ["bar"]}, ++ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), + ] + ) +- def test_verify_uri_qp_localhost_ipv6_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): ++ def test_verify_uri_qp_localhost_ipv6_native_client(self, client_redirect_uri, client_redirect_uri_qp, ++ redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], "application_type": APPLICATION_TYPE_NATIVE} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], ++ "application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ +- ("https://rp.example.com:9999/auth_cb", {"foo":["bar"]}, "http://rp.example.com/auth_cb?foo=bar"), +- ("https://rp.example.com/auth_cb", {"foo":["bar"]}, "http://rp.example.com:9999/auth_cb?foo=bar"), ++ ("https://rp.example.com:9999/auth_cb", {"foo": ["bar"]}, "http://rp.example.com/auth_cb?foo=bar"), ++ ("https://rp.example.com/auth_cb", {"foo": ["bar"]}, "http://rp.example.com:9999/auth_cb?foo=bar"), + ] + ) +- def test_verify_uri_qp_match_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): ++ def test_verify_uri_qp_match_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): + _context = self.endpoint.upstream_get("context") +- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], "application_type": APPLICATION_TYPE_NATIVE} ++ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], ++ "application_type": APPLICATION_TYPE_NATIVE} + + request = {"redirect_uri": redirect_uri} + +@@ -922,7 +946,6 @@ class TestEndpoint(object): + _payload = _jws.jwt.payload() + assert 'aud' in _payload + +- + # def test_audience(self): + # request = AuthorizationRequest( + # client_id="client_id", +@@ -948,6 +971,7 @@ class TestEndpoint(object): + # res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None) + # assert set(res.keys()) == {"session_id", "identity", "user"} + ++ + def test_inputs(): + elems = inputs(dict(foo="bar", home="stead")) + test_elems = ( +diff --git a/tests/test_server_24_oauth2_authorization_endpoint_jar.py b/tests/test_server_24_oauth2_authorization_endpoint_jar.py +index f788c7e..e57360e 100755 +--- a/tests/test_server_24_oauth2_authorization_endpoint_jar.py ++++ b/tests/test_server_24_oauth2_authorization_endpoint_jar.py +@@ -10,6 +10,7 @@ from cryptojwt import JWT + from cryptojwt import KeyJar + from cryptojwt.jwt import utc_time_sans_frac + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import AuthorizationRequest + from idpyoidc.message.oauth2 import JWTSecuredAuthorizationRequest + from idpyoidc.server import Server +@@ -28,15 +29,6 @@ KEYDEFS = [ + + 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", +@@ -58,6 +50,7 @@ USERINFO_db = json.loads(open(full_path("users.json")).read()) + + + class SimpleCookieDealer(object): ++ + def __init__(self, name=""): + self.name = name + +@@ -133,24 +126,29 @@ clients: + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { + "issuer": "https://example.com/", + "password": "mycket hemligt zebra", +- "verify_ssl": False, +- "grant_types_supported": [ +- "authorization_code", +- "implicit", +- "urn:ietf:params:oauth:grant-type:jwt-bearer", +- "refresh_token", +- ], ++ "httpc_params": { ++ "verify": False ++ }, ++ "preference": { ++ "grant_types_supported": [ ++ "authorization_code", ++ "urn:ietf:params:oauth:grant-type:jwt-bearer", ++ "refresh_token", ++ ], ++ "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, ++ "request_object_signing_alg_values_supported": ["HS256"] ++ }, + "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, +- "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, + "request_cls": JWTSecuredAuthorizationRequest, + "endpoint": { + "authorization": { +@@ -190,7 +188,7 @@ class TestEndpoint(object): + context = server.context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = _clients["clients"] +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + self.endpoint = server.get_endpoint("authorization") + self.session_manager = context.session_manager + self.user_id = "diana" +diff --git a/tests/test_server_24_oauth2_resource_indicators.py b/tests/test_server_24_oauth2_resource_indicators.py +index 14e6a03..bb91a46 100644 +--- a/tests/test_server_24_oauth2_resource_indicators.py ++++ b/tests/test_server_24_oauth2_resource_indicators.py +@@ -2,23 +2,14 @@ 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 import KeyJar +-from cryptojwt.jwt import JWT + from cryptojwt.jwt import utc_time_sans_frac +-from cryptojwt.key_jar import init_key_jar +-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 ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import AuthorizationRequest +-from idpyoidc.message.oauth2 import AuthorizationResponse + from idpyoidc.message.oauth2 import TokenErrorResponse + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.server import Server +@@ -26,21 +17,10 @@ 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.authorization import get_uri +-from idpyoidc.server.oauth2.authorization import inputs +-from idpyoidc.server.oauth2.authorization import join_query + from idpyoidc.server.oauth2.authorization import ( + validate_resource_indicators_policy as validate_authorization_resource_indicators_policy, + ) +-from idpyoidc.server.oauth2.authorization import verify_uri + from idpyoidc.server.oauth2.token import Token + from idpyoidc.server.oauth2.token_helper import ( + validate_resource_indicators_policy as validate_token_resource_indicators_policy, +@@ -104,6 +84,7 @@ USERINFO_db = json.loads(open(full_path("users.json")).read()) + + + class SimpleCookieDealer(object): ++ + def __init__(self, name=""): + self.name = name + +@@ -415,6 +396,7 @@ RESOURCE_INDICATORS_ENABLED = { + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=False) + def create_endpoint_ri_disabled(self): + conf = RESOURCE_INDICATORS_DISABLED +@@ -423,9 +405,7 @@ class TestEndpoint(object): + endpoint_context = server.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"] +- ) ++ endpoint_context.keyjar = store_under_other_id(endpoint_context.keyjar, "", conf["issuer"], True) + self.endpoint_context = endpoint_context + self.endpoint = server.get_endpoint("authorization") + self.token_endpoint = server.get_endpoint("token") +@@ -446,9 +426,8 @@ class TestEndpoint(object): + endpoint_context = server.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"] +- ) ++ endpoint_context.keyjar = store_under_other_id(endpoint_context.keyjar, "", ++ conf["issuer"], True) + self.endpoint_context = endpoint_context + self.endpoint = server.get_endpoint("authorization") + self.token_endpoint = server.get_endpoint("token") +@@ -529,7 +508,7 @@ class TestEndpoint(object): + assert msg[key] == request[key] + + def test_authorization_code_req_no_resource_indicators_disabled( +- self, create_endpoint_ri_disabled ++ self, create_endpoint_ri_disabled + ): + """ + Test successful authorization request when resource indicators is disabled. +diff --git a/tests/test_server_24_oauth2_token_endpoint.py b/tests/test_server_24_oauth2_token_endpoint.py +index 4d43d30..e78850a 100644 +--- a/tests/test_server_24_oauth2_token_endpoint.py ++++ b/tests/test_server_24_oauth2_token_endpoint.py +@@ -1,14 +1,15 @@ + import json + import os + ++import pytest + from cryptojwt import JWT + from cryptojwt import KeyJar + from cryptojwt.jws.jws import factory + from cryptojwt.key_jar import build_keyjar +-import pytest + + from idpyoidc.context import OidcContext + from idpyoidc.defaults import JWT_BEARER ++from idpyoidc.key_import import import_jwks + from idpyoidc.message import Message + from idpyoidc.message import REQUIRED_LIST_OF_STRINGS + from idpyoidc.message import SINGLE_REQUIRED_INT +@@ -26,6 +27,7 @@ from idpyoidc.server.authz import AuthzHandling + from idpyoidc.server.client_authn import verify_client + from idpyoidc.server.configure import ASConfiguration + from idpyoidc.server.exception import InvalidToken ++from idpyoidc.server.exception import UnAuthorizedClient + from idpyoidc.server.oauth2.authorization import Authorization + from idpyoidc.server.oauth2.token import Token + from idpyoidc.server.token import handler +@@ -172,6 +174,7 @@ def conf(): + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self, conf): + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) +@@ -184,7 +187,7 @@ class TestEndpoint(object): + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + } +- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") ++ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") + self.session_manager = context.session_manager + self.token_endpoint = server.get_endpoint("token") + self.user_id = "diana" +@@ -342,7 +345,7 @@ class TestEndpoint(object): + _resp = self.token_endpoint.process_request(request=_req) + + # 2nd time used +- with pytest.raises(InvalidToken): ++ with pytest.raises((InvalidToken, UnAuthorizedClient)): + self.token_endpoint.parse_request(_token_request) + + def test_do_refresh_access_token(self): +@@ -855,8 +858,8 @@ CONTEXT.cwd = BASEDIR + CONTEXT.issuer = "https://op.example.com" + CONTEXT.cdb = {"client_1": {}} + KEYJAR = KeyJar() +-KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "client_1") +-KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "") ++KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "client_1") ++KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "") + + + def upstream_get(what, *args): +@@ -919,6 +922,7 @@ def test_jwttoken_2(): + + + class TestClientCredentialsFlow(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self, conf): + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) +@@ -956,6 +960,7 @@ class TestClientCredentialsFlow(object): + + + class TestResourceOwnerPasswordCredentialsFlow(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self, conf): + conf["authentication"] = { +diff --git a/tests/test_server_24_oauth2_token_endpoint_def_conf.py b/tests/test_server_24_oauth2_token_endpoint_def_conf.py +index 9508002..2b181a2 100644 +--- a/tests/test_server_24_oauth2_token_endpoint_def_conf.py ++++ b/tests/test_server_24_oauth2_token_endpoint_def_conf.py +@@ -8,10 +8,11 @@ from cryptojwt.key_jar import build_keyjar + + from idpyoidc.context import OidcContext + from idpyoidc.defaults import JWT_BEARER ++from idpyoidc.key_import import import_jwks ++from idpyoidc.message import Message + from idpyoidc.message import REQUIRED_LIST_OF_STRINGS + from idpyoidc.message import SINGLE_REQUIRED_INT + from idpyoidc.message import SINGLE_REQUIRED_STRING +-from idpyoidc.message import Message + from idpyoidc.message.oauth2 import AccessTokenRequest + from idpyoidc.message.oauth2 import AuthorizationRequest + from idpyoidc.message.oauth2 import CCAccessTokenRequest +@@ -23,6 +24,7 @@ from idpyoidc.server import Server + from idpyoidc.server.authn_event import create_authn_event + from idpyoidc.server.configure import ASConfiguration + from idpyoidc.server.exception import InvalidToken ++from idpyoidc.server.exception import UnAuthorizedClient + from idpyoidc.server.token import handler + from idpyoidc.time_util import utc_time_sans_frac + from tests import CRYPT_CONFIG +@@ -64,6 +66,7 @@ def full_path(local_file): + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +@@ -83,7 +86,7 @@ class TestEndpoint(object): + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + } +- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") ++ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") + self.session_manager = context.session_manager + self.token_endpoint = server.get_endpoint("token") + self.user_id = "diana" +@@ -241,7 +244,7 @@ class TestEndpoint(object): + _resp = self.token_endpoint.process_request(request=_req) + + # 2nd time used +- with pytest.raises(InvalidToken): ++ with pytest.raises((InvalidToken, UnAuthorizedClient)): + self.token_endpoint.parse_request(_token_request) + + def test_do_refresh_access_token(self): +@@ -736,8 +739,8 @@ CONTEXT.cwd = BASEDIR + CONTEXT.issuer = "https://op.example.com" + CONTEXT.cdb = {"client_1": {}} + KEYJAR = KeyJar() +-KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "client_1") +-KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "") ++KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "client_1") ++KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "") + + + def upstream_get(what, *args): +@@ -800,6 +803,7 @@ def test_jwttoken_2(): + + + class TestClientCredentialsFlow(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +@@ -845,6 +849,7 @@ class TestClientCredentialsFlow(object): + + + class TestResourceOwnerPasswordCredentialsFlow(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +diff --git a/tests/test_server_24_oidc_authorization_endpoint.py b/tests/test_server_24_oidc_authorization_endpoint.py +index 7facfcd..73f6524 100755 +--- a/tests/test_server_24_oidc_authorization_endpoint.py ++++ b/tests/test_server_24_oidc_authorization_endpoint.py +@@ -15,6 +15,7 @@ from cryptojwt.utils import b64e + + from idpyoidc.exception import ParameterError + from idpyoidc.exception import URIError ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import AuthorizationErrorResponse + from idpyoidc.message.oauth2 import ResponseMessage + from idpyoidc.message.oidc import AuthorizationRequest +@@ -76,6 +77,8 @@ CAPABILITIES = { + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "refresh_token", + ], ++ "request_uri_parameter_supported": True, ++ "request_object_signing_alg_values_supported": ["HS256"] + } + + CLAIMS = {"id_token": {"given_name": {"essential": True}, "nickname": None}} +@@ -157,7 +160,7 @@ class TestEndpoint(object): + "issuer": "https://example.com/", + "password": "mycket hemligt zebra", + "verify_ssl": False, +- "capabilities": CAPABILITIES, ++ "preference": CAPABILITIES, + "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, + "token_handler_args": { + "jwks_file": "private/token_jwks.json", +@@ -287,7 +290,7 @@ class TestEndpoint(object): + + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = _clients["oidc_clients"] +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + self.context = context + self.endpoint = server.get_endpoint("authorization") + self.session_manager = context.session_manager +@@ -1191,7 +1194,7 @@ class TestACR(object): + + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = _clients["oidc_clients"] +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + self.endpoint = server.get_endpoint("authorization") + self.session_manager = context.session_manager + self.user_id = "diana" +diff --git a/tests/test_server_26_oidc_userinfo_endpoint.py b/tests/test_server_26_oidc_userinfo_endpoint.py +index 1d76e45..3cbb275 100755 +--- a/tests/test_server_26_oidc_userinfo_endpoint.py ++++ b/tests/test_server_26_oidc_userinfo_endpoint.py +@@ -551,8 +551,9 @@ class TestEndpoint(object): + + monkeypatch.setattr("idpyoidc.server.token.utc_time_sans_frac", mock) + +- with pytest.raises(BearerTokenAuthenticationError): +- self.endpoint.parse_request({}, http_info=http_info) ++ res = self.endpoint.parse_request({}, http_info=http_info) ++ assert "error" in res ++ assert res["error"] == "invalid_token" + + def test_userinfo_claims(self): + _acr = "https://refeds.org/profile/mfa" +diff --git a/tests/test_server_30_oidc_end_session.py b/tests/test_server_30_oidc_end_session.py +index 95255a7..abbeb66 100644 +--- a/tests/test_server_30_oidc_end_session.py ++++ b/tests/test_server_30_oidc_end_session.py +@@ -4,11 +4,13 @@ import os + from urllib.parse import parse_qs + from urllib.parse import urlparse + ++from cryptojwt.key_jar import build_keyjar + import pytest + import responses +-from cryptojwt.key_jar import build_keyjar + ++from idpyoidc import alg_info + from idpyoidc.exception import InvalidRequest ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message import Message + from idpyoidc.message.oidc import AuthorizationRequest + from idpyoidc.message.oidc import verified_claim_name +@@ -43,7 +45,7 @@ KEYDEFS = [ + ] + + KEYJAR = build_keyjar(KEYDEFS) +-KEYJAR.import_jwks(KEYJAR.export_jwks(private=True), ISS) ++KEYJAR = store_under_other_id(KEYJAR, "", ISS, True) + + RESPONSE_TYPES_SUPPORTED = [["code"], ["id_token"], ["code", "id_token"]] + +@@ -67,6 +69,7 @@ PREFRERENCES = { + "claims_parameter_supported": True, + "request_parameter_supported": True, + "request_uri_parameter_supported": True, ++ "id_token_signing_alg_values_supported": alg_info.get_signing_algs() + } + + AUTH_REQ = AuthorizationRequest( +diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py +index bf28347..e5b597b 100644 +--- a/tests/test_server_31_oauth2_introspection.py ++++ b/tests/test_server_31_oauth2_introspection.py +@@ -8,6 +8,7 @@ from cryptojwt import as_unicode + from cryptojwt.key_jar import build_keyjar + from cryptojwt.utils import as_bytes + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import TokenIntrospectionRequest + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -207,9 +208,7 @@ class TestEndpoint: + }, + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + } +- server.keyjar.import_jwks_as_json( +- server.keyjar.export_jwks_as_json(private=True), context.issuer +- ) ++ server.keyjar = store_under_other_id(server.keyjar, "", context.issuer, True) + self.introspection_endpoint = server.get_endpoint("introspection") + self.token_endpoint = server.get_endpoint("token") + self.session_manager = context.session_manager +@@ -325,7 +324,7 @@ class TestEndpoint: + assert isinstance(msg_info, dict) + assert set(msg_info.keys()) == {"response", "http_headers"} + assert msg_info["http_headers"] == [ +- ("Content-type", "application/json; charset=utf-8"), ++ ("Content-type", "application/json"), + ("Pragma", "no-cache"), + ("Cache-Control", "no-store"), + ] +diff --git a/tests/test_server_32_oidc_read_registration.py b/tests/test_server_32_oidc_read_registration.py +index e09bc5c..2dea641 100644 +--- a/tests/test_server_32_oidc_read_registration.py ++++ b/tests/test_server_32_oidc_read_registration.py +@@ -160,4 +160,4 @@ class TestEndpoint(object): + + _endp_response = self.registration_api_endpoint.do_response(_info) + assert set(_endp_response.keys()) == {"response", "http_headers"} +- assert ("Content-type", "application/json; charset=utf-8") in _endp_response["http_headers"] ++ assert ("Content-type", "application/json") in _endp_response["http_headers"] +diff --git a/tests/test_server_33_oauth2_pkce.py b/tests/test_server_33_oauth2_pkce.py +index 60cf753..4a08f50 100644 +--- a/tests/test_server_33_oauth2_pkce.py ++++ b/tests/test_server_33_oauth2_pkce.py +@@ -7,6 +7,7 @@ import string + import pytest + import yaml + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message import Message + from idpyoidc.message.oauth2 import AuthorizationErrorResponse + from idpyoidc.message.oidc import AccessTokenRequest +@@ -229,7 +230,7 @@ def create_server(config): + context = server.context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = _clients["oidc_clients"] +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), config["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", config["issuer"], True) + return server + + +diff --git a/tests/test_server_34_oidc_sso.py b/tests/test_server_34_oidc_sso.py +index 4090b51..61fb38d 100755 +--- a/tests/test_server_34_oidc_sso.py ++++ b/tests/test_server_34_oidc_sso.py +@@ -6,6 +6,7 @@ import pytest + import yaml + from cryptojwt import KeyJar + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oidc import AuthorizationRequest + from idpyoidc.server import Server + from idpyoidc.server.configure import OPConfiguration +@@ -199,7 +200,7 @@ class TestUserAuthn(object): + context = server.context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = _clients["oidc_clients"] +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + self.endpoint = server.get_endpoint("authorization") + self.context = context + self.rp_keyjar = KeyJar() +diff --git a/tests/test_server_35_oidc_token_endpoint.py b/tests/test_server_35_oidc_token_endpoint.py +index d6af31e..c2371f3 100755 +--- a/tests/test_server_35_oidc_token_endpoint.py ++++ b/tests/test_server_35_oidc_token_endpoint.py +@@ -6,6 +6,9 @@ import pytest + from cryptojwt import JWT + from cryptojwt.key_jar import build_keyjar + ++from idpyoidc.key_import import import_jwks ++from idpyoidc.server.exception import UnAuthorizedClient ++ + from idpyoidc.defaults import JWT_BEARER + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -212,7 +215,7 @@ class TestEndpoint(_TestEndpoint): + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + } +- self.server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") ++ self.server.keyjar = import_jwks(self.server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") + context.userinfo = USERINFO + self.session_manager = context.session_manager + self.token_endpoint = self.server.get_endpoint("token") +@@ -350,7 +353,7 @@ class TestEndpoint(_TestEndpoint): + _resp = self.token_endpoint.process_request(request=_req) + + # 2nd time used +- with pytest.raises(InvalidToken): ++ with pytest.raises((InvalidToken, UnAuthorizedClient)): + self.token_endpoint.parse_request(_token_request) + + def test_do_refresh_access_token(self): +@@ -1029,7 +1032,7 @@ class TestOldTokens(object): + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + } +- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") ++ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") + self.session_manager = context.session_manager + self.token_endpoint = server.get_endpoint("token") + self.user_id = "diana" +diff --git a/tests/test_server_35_oidc_token_endpoint_def_conf.py b/tests/test_server_35_oidc_token_endpoint_def_conf.py +index f08fefa..74d4e87 100755 +--- a/tests/test_server_35_oidc_token_endpoint_def_conf.py ++++ b/tests/test_server_35_oidc_token_endpoint_def_conf.py +@@ -4,6 +4,9 @@ import pytest + from cryptojwt import JWT + from cryptojwt.key_jar import build_keyjar + ++from idpyoidc.key_import import import_jwks ++from idpyoidc.server.exception import UnAuthorizedClient ++ + from idpyoidc.client.defaults import DEFAULT_KEY_DEFS + from idpyoidc.defaults import JWT_BEARER + from idpyoidc.message.oidc import AccessTokenRequest +@@ -69,7 +72,7 @@ class TestEndpoint: + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], + } +- self.server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") ++ self.server.keyjar = import_jwks(self.server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") + self.session_manager = context.session_manager + self.token_endpoint = self.server.get_endpoint("token") + self.user_id = "diana" +@@ -206,7 +209,7 @@ class TestEndpoint: + _resp = self.token_endpoint.process_request(request=_req) + + # 2nd time used +- with pytest.raises(InvalidToken): ++ with pytest.raises((InvalidToken, UnAuthorizedClient)): + self.token_endpoint.parse_request(_token_request) + + def test_do_refresh_access_token(self): +diff --git a/tests/test_server_36_oauth2_token_exchange.py b/tests/test_server_36_oauth2_token_exchange.py +index 5b3a566..3a2b477 100644 +--- a/tests/test_server_36_oauth2_token_exchange.py ++++ b/tests/test_server_36_oauth2_token_exchange.py +@@ -5,6 +5,7 @@ import pytest + from cryptojwt.jwt import utc_time_sans_frac + from cryptojwt.key_jar import build_keyjar + ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oauth2 import TokenExchangeRequest + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -80,6 +81,7 @@ USERINFO = UserInfo(json.loads(open(full_path("users.json")).read())) + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +@@ -199,7 +201,7 @@ class TestEndpoint(object): + "response_types": ["code", "token", "code id_token", "id_token"], + "allowed_scopes": ["openid", "profile", "offline_access"], + } +- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") ++ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") + self.endpoint = server.get_endpoint("token") + self.introspection_endpoint = server.get_endpoint("introspection") + self.session_manager = self.context.session_manager +@@ -646,8 +648,8 @@ class TestEndpoint(object): + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( +- _resp["error_description"] +- == "Unsupported grant_type: urn:ietf:params:oauth:grant-type:token-exchange" ++ _resp["error_description"] ++ == "Unsupported grant_type: urn:ietf:params:oauth:grant-type:token-exchange" + ) + + def test_wrong_resource(self): +@@ -1374,7 +1376,7 @@ class TestEndpoint(object): + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( +- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" ++ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" + ) + + token_exchange_req["scope"] = "offline_access" +@@ -1454,7 +1456,7 @@ class TestEndpoint(object): + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( +- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" ++ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" + ) + + token_exchange_req["scope"] = "profile" +@@ -1466,7 +1468,7 @@ class TestEndpoint(object): + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( +- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" ++ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" + ) + + token_exchange_req["scope"] = "offline_access" +@@ -1488,5 +1490,5 @@ class TestEndpoint(object): + _resp = self.endpoint.process_request(request=_req) + assert _resp["error"] == "invalid_request" + assert ( +- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" ++ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" + ) +diff --git a/tests/test_server_38_oauth2_revocation_endpoint.py b/tests/test_server_38_oauth2_revocation_endpoint.py +index ad83af1..e41c539 100644 +--- a/tests/test_server_38_oauth2_revocation_endpoint.py ++++ b/tests/test_server_38_oauth2_revocation_endpoint.py +@@ -5,6 +5,7 @@ import pytest + from cryptojwt import as_unicode + from cryptojwt.utils import as_bytes + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import TokenRevocationRequest + from idpyoidc.message.oauth2 import TokenRevocationResponse + from idpyoidc.message.oidc import AccessTokenRequest +@@ -86,6 +87,7 @@ def full_path(local_file): + + @pytest.mark.parametrize("jwt_token", [True, False]) + class TestEndpoint: ++ + @pytest.fixture(autouse=True) + def create_endpoint(self, jwt_token): + conf = { +@@ -216,10 +218,8 @@ class TestEndpoint: + "research_and_scholarship", + ], + } +- endpoint_context.keyjar.import_jwks_as_json( +- endpoint_context.keyjar.export_jwks_as_json(private=True), +- endpoint_context.issuer, +- ) ++ endpoint_context.keyjar = store_under_other_id(endpoint_context.keyjar, "", ++ endpoint_context.issuer, True) + self.revocation_endpoint = server.get_endpoint("token_revocation") + self.token_endpoint = server.get_endpoint("token") + self.session_manager = endpoint_context.session_manager +diff --git a/tests/test_server_40_oauth2_pushed_authorization.py b/tests/test_server_40_oauth2_pushed_authorization.py +index 4d7ea6d..ef7d890 100644 +--- a/tests/test_server_40_oauth2_pushed_authorization.py ++++ b/tests/test_server_40_oauth2_pushed_authorization.py +@@ -7,6 +7,8 @@ from cryptojwt import JWT + from cryptojwt.jwt import remove_jwt_parameters + from cryptojwt.key_jar import init_key_jar + ++from idpyoidc.key_import import import_jwks ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message import Message + from idpyoidc.message.oauth2 import AuthorizationRequest + from idpyoidc.server import Server +@@ -73,6 +75,7 @@ AUTHN_REQUEST = ( + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { +@@ -167,11 +170,11 @@ class TestEndpoint(object): + context = server.context + _clients = yaml.safe_load(io.StringIO(client_yaml)) + context.cdb = verify_oidc_client_information(_clients["oidc_clients"]) +- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) ++ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) + + self.rp_keyjar = init_key_jar(key_defs=KEYDEFS, issuer_id="s6BhdRkqt3") + # Add RP's keys to the OP's keyjar +- server.keyjar.import_jwks(self.rp_keyjar.export_jwks(issuer_id="s6BhdRkqt3"), "s6BhdRkqt3") ++ server.keyjar = import_jwks(server.keyjar, self.rp_keyjar.export_jwks(issuer_id="s6BhdRkqt3"), "s6BhdRkqt3") + + self.pushed_authorization_endpoint = server.get_endpoint("pushed_authorization") + self.authorization_endpoint = server.get_endpoint("authorization") +@@ -251,7 +254,7 @@ class TestEndpoint(object): + + # And now for the authorization request with the OP provided request_uri + +- _msg["request_uri"] = _resp["http_response"]["request_uri"] ++ _msg["request_uri"] = _resp["response_args"]["request_uri"] + for parameter in ["code_challenge", "code_challenge_method"]: + del _msg[parameter] + +diff --git a/tests/test_server_50_persistence.py b/tests/test_server_50_persistence.py +index adc31d9..c8ca5a5 100644 +--- a/tests/test_server_50_persistence.py ++++ b/tests/test_server_50_persistence.py +@@ -6,6 +6,7 @@ import pytest + from cryptojwt.jwt import utc_time_sans_frac + from cryptojwt.key_jar import init_key_jar + ++from idpyoidc.key_import import import_jwks_as_json + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest + from idpyoidc.server import Server +@@ -212,6 +213,7 @@ ENDPOINT_CONTEXT_CONFIG = { + + + class TestEndpoint(object): ++ + @pytest.fixture(autouse=True) + def create_endpoint(self): + try: +@@ -221,9 +223,9 @@ class TestEndpoint(object): + + # Both have to use the same keyjar + _keyjar = init_key_jar(key_defs=KEYDEFS) +- _keyjar.import_jwks_as_json( +- _keyjar.export_jwks_as_json(True, ""), ENDPOINT_CONTEXT_CONFIG["issuer"] +- ) ++ _keyjar = import_jwks_as_json(_keyjar, ++ _keyjar.export_jwks_as_json(True, ""), ++ ENDPOINT_CONTEXT_CONFIG["issuer"]) + server1 = Server( + OPConfiguration(conf=ENDPOINT_CONTEXT_CONFIG, base_path=BASEDIR), + cwd=BASEDIR, +@@ -351,14 +353,15 @@ class TestEndpoint(object): + + def test_init(self): + assert self.endpoint[1] +- assert set(self.endpoint[1].upstream_get("context").provider_info["scopes_supported"]) == { +- "openid" +- } ++ ++ _context_1 = self.endpoint[1].upstream_get("context") ++ _context_2 = self.endpoint[2].upstream_get("context") ++ + assert ( +- self.endpoint[1].upstream_get("context").provider_info["claims_parameter_supported"] +- == self.endpoint[2].upstream_get("context").provider_info[ +- "claims_parameter_supported"] ++ _context_1.provider_info["claims_parameter_supported"] == _context_2.provider_info[ ++ "claims_parameter_supported"] + ) ++ print(_context_1.provider_info.get("claims_parameter_supported")) + + def test_parse(self): + session_id = self._create_session(AUTH_REQ, index=1) +diff --git a/tests/test_server_60_dpop.py b/tests/test_server_60_dpop.py +index e13b8a3..15ded45 100644 +--- a/tests/test_server_60_dpop.py ++++ b/tests/test_server_60_dpop.py +@@ -1,11 +1,14 @@ + import os + +-import pytest + from cryptojwt.jwk.ec import ECKey + from cryptojwt.jwk.ec import new_ec_key + from cryptojwt.jws.jws import factory + from cryptojwt.key_jar import init_key_jar ++import pytest + ++from idpyoidc.client.defaults import DEFAULT_KEY_DEFS ++from idpyoidc.client.oauth2 import Client ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import AccessTokenRequest + from idpyoidc.message.oauth2 import AuthorizationRequest + from idpyoidc.server import Server +@@ -14,14 +17,16 @@ from idpyoidc.server.authn_event import create_authn_event + from idpyoidc.server.client_authn import verify_client + from idpyoidc.server.configure import OPConfiguration + from idpyoidc.server.oauth2.add_on.dpop import DPoPProof +-from idpyoidc.server.oauth2.add_on.dpop import token_post_parse_request +-from idpyoidc.server.oauth2.authorization import Authorization ++from idpyoidc.server.oidc.authorization import Authorization + from idpyoidc.server.oidc.token import Token ++from idpyoidc.server.oidc.userinfo import UserInfo + from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD + from idpyoidc.time_util import utc_time_sans_frac + from tests import CRYPT_CONFIG + from tests import SESSION_PARAMS + ++_dirname = os.path.dirname(os.path.abspath(__file__)) ++ + DPOP_HEADER = ( + "eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMz" + "R0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFq" +@@ -54,43 +59,13 @@ def test_verify_header(): + assert _dpop["htm"] == _dpop3["htm"] + + +-KEYDEFS = [ +- {"type": "RSA", "key": "", "use": ["sig"]}, +- {"type": "EC", "crv": "P-256", "use": ["sig"]}, +-] +- + ISSUER = "https://example.com/" + +-KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) +-KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") +- +-RESPONSE_TYPES_SUPPORTED = [ +- ["code"], +- ["id_token"], +- ["code", "id_token"], +-] +- +-CAPABILITIES = { +- "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], +- "token_endpoint_auth_methods_supported": [ +- "client_secret_post", +- "client_secret_basic", +- "client_secret_jwt", +- "private_key_jwt", +- ], +- "response_modes_supported": ["query", "fragment", "form_post"], +- "subject_types_supported": ["public", "pairwise", "ephemeral"], +- "claim_types_supported": ["normal", "aggregated", "distributed"], +- "claims_parameter_supported": True, +- "request_parameter_supported": True, +- # "request_uri_parameter_supported": True, +-} ++KEYJAR = init_key_jar(key_defs=DEFAULT_KEY_DEFS, issuer_id=ISSUER) ++KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) + + AUTH_REQ = AuthorizationRequest( +- client_id="client_1", +- redirect_uri="https://example.com/cb", + scope=["openid"], +- state="STATE", + response_type="code", + ) + +@@ -105,90 +80,174 @@ TOKEN_REQ = AccessTokenRequest( + BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +-class TestEndpoint(object): +- @pytest.fixture(autouse=True) +- def create_endpoint(self): +- conf = { +- "issuer": ISSUER, +- "httpc_params": {"verify": False, "timeout": 1}, +- "capabilities": CAPABILITIES, +- "add_on": { +- "dpop": { +- "function": "idpyoidc.server.oauth2.add_on.dpop.add_support", +- "kwargs": {"dpop_signing_alg_values_supported": ["ES256"]}, +- }, ++def create_client(): ++ config = { ++ "client_id": "client_1", ++ "client_secret": "a longesh password", ++ "redirect_uris": ["https://example.com/cli/authz_cb"], ++ "preference": {"response_types": ["code"]}, ++ "add_ons": { ++ "dpop": { ++ "function": "idpyoidc.client.oauth2.add_on.dpop.add_support", ++ "kwargs": {"dpop_signing_alg_values_supported": ["ES256", "ES512"]}, ++ } ++ }, ++ "client_authn_methods": { ++ "dpop": { ++ "class": "idpyoidc.client.oauth2.add_on.dpop.DPoPClientAuth", ++ "kwargs": {} ++ } ++ } ++ } ++ ++ services = { ++ "discovery": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"}, ++ "authorization": {"class": "idpyoidc.client.oauth2.authorization.Authorization"}, ++ "access_token": {"class": "idpyoidc.client.oauth2.access_token.AccessToken"}, ++ "refresh_access_token": { ++ "class": "idpyoidc.client.oauth2.refresh_access_token.RefreshAccessToken" ++ }, ++ "userinfo": {"class": "idpyoidc.client.oidc.userinfo.UserInfo"}, ++ } ++ ++ CLI_KEY = init_key_jar( ++ public_path="{}/pub_client.jwks".format(_dirname), ++ private_path="{}/priv_client.jwks".format(_dirname), ++ key_defs=DEFAULT_KEY_DEFS, ++ issuer_id="client_id", ++ ) ++ ++ client = Client(keyjar=CLI_KEY, config=config, services=services) ++ ++ client.get_context().provider_info = { ++ "authorization_endpoint": "https://example.com/auth", ++ "token_endpoint": "https://example.com/token", ++ "dpop_signing_alg_values_supported": ["RS256", "ES256"], ++ "userinfo_endpoint": "https://example.com/user", ++ } ++ ++ return client ++ ++ ++def create_server(): ++ RESPONSE_TYPES_SUPPORTED = [ ++ ["code"], ++ ["id_token"], ++ ["code", "id_token"], ++ ] ++ ++ CAPABILITIES = { ++ "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], ++ "token_endpoint_auth_methods_supported": [ ++ "client_secret_post", ++ "client_secret_basic", ++ "client_secret_jwt", ++ "private_key_jwt", ++ ], ++ "response_modes_supported": ["query", "fragment", "form_post"], ++ "subject_types_supported": ["public", "pairwise", "ephemeral"], ++ "claim_types_supported": ["normal", "aggregated", "distributed"], ++ "claims_parameter_supported": True, ++ "request_parameter_supported": True, ++ # "request_uri_parameter_supported": True, ++ "client_authn_methods": { ++ "dpop": { ++ "class": "idpyoidc.server.oauth2.add_on.dpop.DPoPClientAuth" ++ } ++ } ++ } ++ ++ conf = { ++ "issuer": ISSUER, ++ "httpc_params": {"verify": False, "timeout": 1}, ++ "preference": CAPABILITIES, ++ "add_on": { ++ "dpop": { ++ "function": "idpyoidc.server.oauth2.add_on.dpop.add_support", ++ "kwargs": {"dpop_signing_alg_values_supported": ["ES256"]}, + }, +- "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, +- "token_handler_args": { +- "jwks_file": "private/token_jwks.json", +- "code": {"lifetime": 600, "kwargs": {"crypt_conf": CRYPT_CONFIG}}, +- "token": { +- "class": "idpyoidc.server.token.jwt_token.JWTToken", +- "kwargs": { +- "lifetime": 3600, +- "base_claims": {"eduperson_scoped_affiliation": None}, +- "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}, +- } +- }, ++ }, ++ "keys": {"uri_path": "jwks.json", "key_defs": DEFAULT_KEY_DEFS}, ++ "token_handler_args": { ++ "jwks_file": "private/token_jwks.json", ++ "code": {"lifetime": 600, "kwargs": {"crypt_conf": CRYPT_CONFIG}}, ++ "token": { ++ "class": "idpyoidc.server.token.jwt_token.JWTToken", ++ "kwargs": { ++ "lifetime": 3600, ++ "base_claims": {"eduperson_scoped_affiliation": None}, ++ "add_claims_by_scope": True, ++ "aud": ["https://example.org/appl"], + }, + }, +- "endpoint": { +- "authorization": { +- "path": "{}/authorization", +- "class": Authorization, +- "kwargs": {}, ++ "refresh": { ++ "class": "idpyoidc.server.token.jwt_token.JWTToken", ++ "kwargs": { ++ "lifetime": 3600, ++ "aud": ["https://example.org/appl"], + }, +- "token": { +- "path": "{}/token", +- "class": Token, +- "kwargs": {"client_authn_method": ["none"]}, ++ }, ++ "id_token": { ++ "class": "idpyoidc.server.token.id_token.IDToken", ++ "kwargs": { ++ "base_claims": { ++ "email": {"essential": True}, ++ "email_verified": {"essential": True}, ++ } + }, + }, +- "client_authn": verify_client, +- "authentication": { +- "anon": { +- "acr": INTERNETPROTOCOLPASSWORD, +- "class": "idpyoidc.server.user_authn.user.NoAuthn", +- "kwargs": {"user": "diana"}, +- } ++ }, ++ "endpoint": { ++ "authorization": { ++ "path": "{}/authorization", ++ "class": Authorization, ++ "kwargs": {}, + }, +- "template_dir": "template", +- "userinfo": { +- "class": user_info.UserInfo, +- "kwargs": {"db_file": "users.json"}, ++ "token": { ++ "path": "{}/token", ++ "class": Token, ++ "kwargs": {"client_authn_method": ["client_secret_basic"]}, + }, +- "session_params": SESSION_PARAMS, +- } +- server = Server(OPConfiguration(conf, base_path=BASEDIR), keyjar=KEYJAR) +- self.context = server.context +- self.context.cdb["client_1"] = { +- "client_secret": "hemligt", +- "redirect_uris": [("https://example.com/cb", None)], +- "client_salt": "salted", +- "token_endpoint_auth_method": "client_secret_post", +- "response_types": ["code", "token", "code id_token", "id_token"], +- "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], +- } ++ "user_info": { ++ "path": "{}/user", ++ "class": UserInfo, ++ "kwargs": {"client_authn_method": ["dpop"]}, ++ }, ++ }, ++ "client_authn": verify_client, ++ "authentication": { ++ "anon": { ++ "acr": INTERNETPROTOCOLPASSWORD, ++ "class": "idpyoidc.server.user_authn.user.NoAuthn", ++ "kwargs": {"user": "diana"}, ++ } ++ }, ++ "template_dir": "template", ++ "userinfo": { ++ "class": user_info.UserInfo, ++ "kwargs": {"db_file": "users.json"}, ++ }, ++ "session_params": SESSION_PARAMS, ++ } ++ server = Server(OPConfiguration(conf, base_path=BASEDIR), keyjar=KEYJAR) ++ return server ++ ++ ++class TestEndpoint(object): ++ @pytest.fixture(autouse=True) ++ def create_setup(self): ++ self.server = create_server() + self.user_id = "diana" +- self.token_endpoint = server.get_endpoint("token") ++ self.token_endpoint = self.server.get_endpoint("token") ++ self.user_info_endpoint = self.server.get_endpoint("userinfo") ++ ++ self.client = create_client() ++ self.context = self.server.context ++ self.context.cdb["client_1"] = self.client.context.prefers() + self.session_manager = self.context.session_manager + ++ self.authz_service = self.client.get_service("authorization") ++ + def _create_session(self, auth_req, sub_type="public", sector_identifier=""): + if sector_identifier: + authz_req = auth_req.copy() +@@ -201,72 +260,79 @@ class TestEndpoint(object): + ae, authz_req, 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") +- ++ def _mint_token(self, token_class, grant, session_id, based_on=None, **kwargs): + # Constructing an authorization code is now done +- _code = grant.mint_token( ++ return grant.mint_token( + session_id=session_id, +- context=self.context, +- token_class="authorization_code", +- token_handler=self.session_manager.token_handler["authorization_code"], +- usage_rules=usage_rules, ++ context=self.token_endpoint.upstream_get("context"), ++ token_class=token_class, ++ token_handler=self.session_manager.token_handler.handler[token_class], ++ expires_at=utc_time_sans_frac() + 300, # 5 minutes from now ++ based_on=based_on, ++ **kwargs + ) + +- 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 _access_token_request_response(self): ++ # Authz ++ auth_req = AUTH_REQ.copy() ++ auth_req["client_id"] = self.client.client_id ++ _redirect_uri = self.client.context.claims.get_preference("redirect_uris")[0] ++ auth_req["redirect_uri"] = _redirect_uri ++ _context = self.client.context ++ auth_req["state"] = _context.cstate.create_state(iss=_context.get("issuer")) ++ session_id = self._create_session(auth_req) ++ # Consent handling ++ grant = self.token_endpoint.upstream_get("endpoint_context").authz(session_id, auth_req) ++ self.session_manager[session_id] = grant ++ code = self._mint_token("authorization_code", grant, session_id) ++ _context.cstate.update(auth_req["state"], auth_req) ++ ++ # Access token request from the RP ++ token_serv = self.client.get_service("accesstoken") ++ req_args = { ++ "grant_type": "authorization_code", ++ "code": code.value, ++ "redirect_uri": _redirect_uri ++ } ++ req_info = token_serv.get_request_parameters(request_args=req_args, state=auth_req["state"]) ++ assert "headers" in req_info ++ assert "dpop" in req_info["headers"] ++ ++ # On the OP's side ++ req = self.token_endpoint.parse_request( ++ req_args, ++ http_info={"headers": req_info["headers"], "url": _redirect_uri, "method": "POST"}) ++ resp = self.token_endpoint.process_request(req) ++ _context.cstate.update(auth_req["state"], resp["response_args"]) ++ return resp, auth_req["state"] + + def test_post_parse_request(self): +- auth_req = token_post_parse_request( +- AUTH_REQ, +- AUTH_REQ["client_id"], +- self.context, +- http_info={ +- "headers": {"dpop": DPOP_HEADER}, +- "url": "https://server.example.com/token", +- "method": "POST", +- }, +- ) +- assert auth_req +- assert "dpop_jkt" in auth_req ++ # DPoP Access Token Request ++ _response, state = self._access_token_request_response() ++ assert "response_args" in _response + + def test_process_request(self): +- session_id = self._create_session(AUTH_REQ) +- grant = self.session_manager[session_id] +- code = self._mint_code(grant, AUTH_REQ["client_id"]) +- +- _token_request = TOKEN_REQ.to_dict() +- _context = self.context +- _token_request["code"] = code.value +- _req = self.token_endpoint.parse_request( +- _token_request, +- http_info={ +- "headers": {"dpop": DPOP_HEADER}, +- "url": "https://server.example.com/token", +- "method": "POST", +- }, +- ) ++ _response, state = self._access_token_request_response() ++ ++ # The RP creates the user info request ++ _user_info_service = self.client.get_service("userinfo") ++ _request = _user_info_service.get_request_parameters(state=state, authn_method="dpop") + +- assert "dpop_jkt" in _req ++ http_info = { ++ "headers": _request["headers"], ++ "method": _request["method"], ++ "url": _request["url"] ++ } + +- _resp = self.token_endpoint.process_request(request=_req) +- assert _resp["response_args"]["token_type"] == "DPoP" ++ assert set(http_info["headers"].keys()) == {"Authorization", "dpop"} ++ assert http_info["headers"]["Authorization"].startswith("DPoP ") + +- access_token = _resp["response_args"]["access_token"] +- jws = factory(access_token) +- _payload = jws.jwt.payload() +- assert "cnf" in _payload +- assert _payload["cnf"]["jkt"] == _req["dpop_jkt"] ++ _jws = factory(http_info["headers"]["dpop"]) ++ _payload = _jws.jwt.payload() ++ assert "htm" in _payload ++ assert "htu" in _payload + +- # Make sure DPoP also is in the session access token instance. +- _session_info = self.session_manager.get_session_info_by_token( +- access_token, handler_key="access_token" +- ) +- _token = self.session_manager.find_token(_session_info["branch_id"], access_token) +- assert _token.token_type == "DPoP" ++ _req = self.user_info_endpoint.parse_request(request=_request, http_info=http_info) ++ _resp = self.user_info_endpoint.process_request(_req) ++ assert _resp["response_args"] ++ assert "sub" in _resp["response_args"] +diff --git a/tests/test_server_61_add_on.py b/tests/test_server_61_add_on.py +index 9af7206..ac91951 100644 +--- a/tests/test_server_61_add_on.py ++++ b/tests/test_server_61_add_on.py +@@ -4,6 +4,7 @@ from urllib.parse import urlparse + import pytest + from cryptojwt.key_jar import init_key_jar + ++from idpyoidc.key_import import store_under_other_id + from idpyoidc.message.oauth2 import AuthorizationRequest + from idpyoidc.message.oauth2 import AuthorizationResponse + from idpyoidc.server import Server +@@ -24,7 +25,7 @@ KEYDEFS = [ + ISSUER = "https://example.com/" + + KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) +-KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") ++KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) + + RESPONSE_TYPES_SUPPORTED = [ + ["code"], +diff --git a/tests/test_tandem_oauth2_add_on.py b/tests/test_tandem_oauth2_add_on.py +index a3776fc..000ab8a 100644 +--- a/tests/test_tandem_oauth2_add_on.py ++++ b/tests/test_tandem_oauth2_add_on.py +@@ -5,6 +5,7 @@ from typing import List + from cryptojwt.key_jar import build_keyjar + + from idpyoidc.client.oauth2 import Client ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oauth2 import is_error_message + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -169,6 +170,7 @@ CLIENT_CONFIG = { + + + class Flow(object): ++ + def __init__(self, client, server): + self.client = client + self.server = server +@@ -290,7 +292,7 @@ def test_pkce(): + ) + + server.context.cdb["client"] = CLIENT_CONFIG +- server.context.keyjar.import_jwks(client.keyjar.export_jwks(), "client") ++ server.context.keyjar = import_jwks(server.context.keyjar, client.keyjar.export_jwks(), "client") + + server.context.set_provider_info() + +@@ -332,7 +334,7 @@ def test_jar(): + ) + + server.context.cdb["client"] = CLIENT_CONFIG +- server.context.keyjar.import_jwks(client.keyjar.export_jwks(), "client") ++ server.context.keyjar = import_jwks(server.context.keyjar, client.keyjar.export_jwks(), "client") + + server.context.set_provider_info() + +diff --git a/tests/test_tandem_oauth2_code.py b/tests/test_tandem_oauth2_code.py +index 091a004..39bc376 100644 +--- a/tests/test_tandem_oauth2_code.py ++++ b/tests/test_tandem_oauth2_code.py +@@ -5,6 +5,7 @@ import pytest + from cryptojwt.key_jar import build_keyjar + + from idpyoidc.client.oauth2 import Client ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oauth2 import is_error_message + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -13,7 +14,6 @@ from idpyoidc.server import Server + from idpyoidc.server.authz import AuthzHandling + from idpyoidc.server.client_authn import verify_client + from idpyoidc.server.configure import ASConfiguration +-from idpyoidc.server.cookie_handler import CookieHandler + from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD + from idpyoidc.server.user_info import UserInfo + from idpyoidc.util import rndstr +@@ -72,6 +72,7 @@ _OAUTH2_SERVICES = { + + + class TestFlow(object): ++ + @pytest.fixture(autouse=True) + def create_entities(self): + server_conf = { +@@ -170,7 +171,7 @@ class TestFlow(object): + + self.context = self.server.context + self.context.cdb["client_1"] = client_1_config +- self.context.keyjar.import_jwks(self.client.keyjar.export_jwks(), "client_1") ++ self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") + + self.context.set_provider_info() + self.session_manager = self.context.session_manager +diff --git a/tests/test_tandem_oauth2_par_service.py b/tests/test_tandem_oauth2_par_service.py +new file mode 100644 +index 0000000..9630a55 +--- /dev/null ++++ b/tests/test_tandem_oauth2_par_service.py +@@ -0,0 +1,285 @@ ++import json ++import os ++ ++import pytest ++from cryptojwt.key_jar import build_keyjar ++ ++from idpyoidc.client.oauth2 import Client ++from idpyoidc.key_import import import_jwks ++from idpyoidc.message.oauth2 import is_error_message ++from idpyoidc.message.oidc import AccessTokenRequest ++from idpyoidc.message.oidc import AuthorizationRequest ++from idpyoidc.message.oidc import RefreshAccessTokenRequest ++from idpyoidc.server import Server ++from idpyoidc.server.authz import AuthzHandling ++from idpyoidc.server.client_authn import verify_client ++from idpyoidc.server.configure import ASConfiguration ++from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD ++from idpyoidc.server.user_info import UserInfo ++from idpyoidc.util import rndstr ++from tests import CRYPT_CONFIG ++from tests import SESSION_PARAMS ++ ++KEYDEFS = [ ++ {"type": "RSA", "key": "", "use": ["sig"]}, ++ {"type": "EC", "crv": "P-256", "use": ["sig"]}, ++] ++ ++CLIENT_KEYJAR = build_keyjar(KEYDEFS) ++ ++COOKIE_KEYDEFS = [ ++ {"type": "oct", "kid": "sig", "use": ["sig"]}, ++ {"type": "oct", "kid": "enc", "use": ["enc"]}, ++] ++ ++AUTH_REQ = AuthorizationRequest( ++ client_id="client_1", ++ redirect_uri="https://example.com/cb", ++ scope=["openid"], ++ state="STATE", ++ response_type="code", ++) ++ ++TOKEN_REQ = AccessTokenRequest( ++ client_id="client_1", ++ redirect_uri="https://example.com/cb", ++ state="STATE", ++ grant_type="authorization_code", ++ client_secret="hemligt", ++) ++ ++REFRESH_TOKEN_REQ = RefreshAccessTokenRequest( ++ grant_type="refresh_token", client_id="https://example.com/", client_secret="hemligt" ++) ++ ++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 = UserInfo(json.loads(open(full_path("users.json")).read())) ++ ++_OAUTH2_SERVICES = { ++ "metadata": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"}, ++ "authorization": {"class": "idpyoidc.client.oauth2.authorization.Authorization"}, ++ "pushed_authorization": {"class": "idpyoidc.client.oauth2.pushed_authorization.PushedAuthorization"}, ++ "access_token": {"class": "idpyoidc.client.oauth2.access_token.AccessToken"}, ++ "resource": {"class": "idpyoidc.client.oauth2.resource.Resource"}, ++} ++ ++ ++class TestFlow(object): ++ ++ @pytest.fixture(autouse=True) ++ def create_entities(self): ++ server_conf = { ++ "issuer": "https://example.com/", ++ "httpc_params": {"verify": False, "timeout": 1}, ++ "subject_types_supported": ["public", "pairwise", "ephemeral"], ++ "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, ++ "endpoint": { ++ "metadata": { ++ "path": ".well-known/oauth-authorization-server", ++ "class": "idpyoidc.server.oauth2.server_metadata.ServerMetadata", ++ "kwargs": {}, ++ }, ++ "authorization": { ++ "path": "authorization", ++ "class": "idpyoidc.server.oauth2.authorization.Authorization", ++ "kwargs": {}, ++ }, ++ "pushed_authorization": { ++ "path": "par", ++ "class": "idpyoidc.server.oauth2.pushed_authorization.PushedAuthorization", ++ "kwargs": {}, ++ }, ++ "token": { ++ "path": "token", ++ "class": "idpyoidc.server.oauth2.token.Token", ++ "kwargs": {}, ++ }, ++ }, ++ "authentication": { ++ "anon": { ++ "acr": INTERNETPROTOCOLPASSWORD, ++ "class": "idpyoidc.server.user_authn.user.NoAuthn", ++ "kwargs": {"user": "diana"}, ++ } ++ }, ++ "userinfo": {"class": UserInfo, "kwargs": {"db": {}}}, ++ "client_authn": verify_client, ++ "authz": { ++ "class": AuthzHandling, ++ "kwargs": { ++ "grant_config": { ++ "usage_rules": { ++ "authorization_code": { ++ "supports_minting": ["access_token", "refresh_token"], ++ "max_usage": 1, ++ }, ++ "access_token": { ++ "supports_minting": ["access_token", "refresh_token"], ++ "expires_in": 600, ++ }, ++ "refresh_token": { ++ "supports_minting": ["access_token"], ++ "audience": ["https://example.com", "https://example2.com"], ++ "expires_in": 43200, ++ }, ++ }, ++ "expires_in": 43200, ++ } ++ }, ++ }, ++ "token_handler_args": { ++ "jwks_file": "private/token_jwks.json", ++ "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"], ++ }, ++ }, ++ }, ++ "session_params": SESSION_PARAMS, ++ } ++ self.server = Server(ASConfiguration(conf=server_conf, base_path=BASEDIR), cwd=BASEDIR) ++ ++ client_1_config = { ++ "issuer": server_conf["issuer"], ++ "client_secret": "hemligtlösenord", ++ "client_id": "client_1", ++ "redirect_uris": ["https://example.com/cb"], ++ "client_salt": "salted_peanuts_cooking", ++ "token_endpoint_auth_methods_supported": ["client_secret_post"], ++ "response_types_supported": ["code"], ++ } ++ client_services = _OAUTH2_SERVICES ++ self.client = Client( ++ client_type="oauth2", ++ config=client_1_config, ++ keyjar=build_keyjar(KEYDEFS), ++ services=_OAUTH2_SERVICES, ++ ) ++ ++ self.context = self.server.context ++ self.context.cdb["client_1"] = client_1_config ++ self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") ++ ++ self.context.set_provider_info() ++ self.session_manager = self.context.session_manager ++ self.user_id = "diana" ++ ++ def do_query(self, service_type, endpoint_type, request_args, state): ++ _client_service = self.client.get_service(service_type) ++ req_info = _client_service.get_request_parameters(request_args=request_args, state=state) ++ ++ areq = req_info.get("request") ++ headers = req_info.get("headers") ++ ++ _server_endpoint = self.server.get_endpoint(endpoint_type) ++ if areq: ++ if headers: ++ argv = {"http_info": {"headers": headers}} ++ else: ++ argv = {} ++ areq.lax = True ++ _req = areq.serialize(_server_endpoint.request_format) ++ _pr_resp = _server_endpoint.parse_request(_req, **argv) ++ else: ++ _pr_resp = _server_endpoint.parse_request(areq) ++ ++ if is_error_message(_pr_resp): ++ return areq, _pr_resp ++ ++ _resp = _server_endpoint.process_request(_pr_resp) ++ if is_error_message(_resp): ++ return areq, _resp ++ ++ _response = _server_endpoint.do_response(**_resp) ++ ++ resp = _client_service.parse_response(_response["response"]) ++ _client_service.update_service_context(_resp["response_args"], key=state) ++ return areq, resp ++ ++ def process_setup(self, token=None, scope=None): ++ # ***** Discovery ********* ++ ++ _req, _resp = self.do_query("server_metadata", "server_metadata", {}, "") ++ ++ # ***** Pushed Authorization Request ********** ++ _nonce = (rndstr(24),) ++ _context = self.client.get_service_context() ++ # Need a new state for a new authorization request ++ _state = _context.cstate.create_state(iss=_context.get("issuer")) ++ _context.cstate.bind_key(_nonce, _state) ++ ++ req_args = {"response_type": ["code"], "nonce": _nonce, "state": _state} ++ ++ if scope: ++ _scope = scope ++ else: ++ _scope = ["openid"] ++ ++ if token and list(token.keys())[0] == "refresh_token": ++ _scope = ["openid", "offline_access"] ++ ++ req_args["scope"] = _scope ++ ++ areq, auth_response = self.do_query("pushed_authorization", ++ "pushed_authorization", ++ req_args, ++ _state) ++ ++ # ***** Authorization Request ********** ++ _context = self.client.get_service_context() ++ ++ req_args = {"request_uri": auth_response["request_uri"], "response_type": ["code"]} ++ ++ areq, auth_response = self.do_query("authorization", "authorization", req_args, _state) ++ ++ # ***** Token Request ********** ++ ++ req_args = { ++ "code": auth_response["code"], ++ "state": auth_response["state"], ++ "redirect_uri": areq["redirect_uri"], ++ "grant_type": "authorization_code", ++ "client_id": self.client.get_client_id(), ++ "client_secret": _context.get_usage("client_secret"), ++ } ++ ++ _token_request, resp = self.do_query("accesstoken", "token", req_args, _state) ++ ++ return resp, _state, _scope ++ ++ def test_flow(self): ++ """ ++ Test that token exchange requests work correctly ++ """ ++ ++ resp, _state, _scope = self.process_setup(token="access_token", scope=["foobar"]) ++ ++ # Construct the resource request ++ ++ _client_service = self.client.get_service("resource") ++ req_info = _client_service.get_request_parameters( ++ authn_method="bearer_header", state=_state, endpoint="https://resource.example.com" ++ ) ++ ++ assert req_info["url"] == "https://resource.example.com" ++ assert "Authorization" in req_info["headers"] ++ assert req_info["headers"]["Authorization"].startswith("Bearer") +diff --git a/tests/test_tandem_oauth2_token_exchange.py b/tests/test_tandem_oauth2_token_exchange.py +index 6b722d3..15e9e67 100644 +--- a/tests/test_tandem_oauth2_token_exchange.py ++++ b/tests/test_tandem_oauth2_token_exchange.py +@@ -5,6 +5,7 @@ import pytest + from cryptojwt.key_jar import build_keyjar + + from idpyoidc.client.oauth2 import Client ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oauth2 import is_error_message + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -212,8 +213,8 @@ class TestEndpoint(object): + self.context = self.server.context + self.context.cdb["client_1"] = client_1_config + self.context.cdb["client_2"] = client_2_config +- self.context.keyjar.import_jwks(self.client_1.keyjar.export_jwks(), "client_1") +- self.context.keyjar.import_jwks(self.client_2.keyjar.export_jwks(), "client_2") ++ self.context.keyjar = import_jwks(self.context.keyjar, self.client_1.keyjar.export_jwks(), "client_1") ++ self.context.keyjar = import_jwks(self.context.keyjar, self.client_2.keyjar.export_jwks(), "client_2") + + self.context.set_provider_info() + +diff --git a/tests/test_tandem_oauth2_token_revocation.py b/tests/test_tandem_oauth2_token_revocation.py +index 92d4430..6845cd9 100644 +--- a/tests/test_tandem_oauth2_token_revocation.py ++++ b/tests/test_tandem_oauth2_token_revocation.py +@@ -4,6 +4,7 @@ import pytest + from cryptojwt.key_jar import build_keyjar + + from idpyoidc.client.oauth2 import Client ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oauth2 import is_error_message + from idpyoidc.server import ASConfiguration + from idpyoidc.server import Server +@@ -137,7 +138,7 @@ class TestClient(object): + # ------- tell the server about the client ---------------- + self.context = self.server.context + self.context.cdb["client_1"] = client_conf +- self.context.keyjar.import_jwks(self.client.keyjar.export_jwks(), "client_1") ++ self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") + + def do_query(self, service_type, endpoint_type, request_args, state): + _client = self.client.get_service(service_type) +diff --git a/tests/test_tandem_oidc_code.py b/tests/test_tandem_oidc_code.py +index 5f11f4a..9b575aa 100644 +--- a/tests/test_tandem_oidc_code.py ++++ b/tests/test_tandem_oidc_code.py +@@ -5,6 +5,7 @@ import pytest + from cryptojwt.key_jar import build_keyjar + + from idpyoidc.client.oidc import RP ++from idpyoidc.key_import import import_jwks + from idpyoidc.message.oauth2 import is_error_message + from idpyoidc.message.oidc import AccessTokenRequest + from idpyoidc.message.oidc import AuthorizationRequest +@@ -74,6 +75,7 @@ _OIDC_SERVICES = { + + + class TestFlow(object): ++ + @pytest.fixture(autouse=True) + def create_entities(self): + server_conf = { +@@ -81,6 +83,7 @@ class TestFlow(object): + "httpc_params": {"verify": False, "timeout": 1}, + "subject_types_supported": ["public", "pairwise", "ephemeral"], + "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, ++ "scopes_supported": ["openid", "profile", "email", "offline_access", "address", "phone"], + "endpoint": { + "provider_info": { + "path": ".well-known/openid-configuration", +@@ -180,12 +183,13 @@ class TestFlow(object): + "redirect_uris": ["https://example.com/cb"], + "token_endpoint_auth_methods_supported": ["client_secret_post"], + "response_types_supported": ["code", "id_token", "id_token token"], ++ "preference": {"scopes_supported": ["openid", "profile"]} + } + self.rp = RP(config=client_config, keyjar=build_keyjar(KEYDEFS), services=_OIDC_SERVICES) + + self.context = self.server.context + # self.context.cdb["client_1"] = client_config +- # self.context.keyjar.import_jwks(self.rp.keyjar.export_jwks(), "client_1") ++ # self.context.keyjar = import_jwks(self.context.keyjar, self.rp.keyjar.export_jwks(), "client_1") + + self.context.set_provider_info() + # self.session_manager = self.context.session_manager +@@ -225,10 +229,11 @@ class TestFlow(object): + _client_service.update_service_context(_resp["response_args"], key=state) + # Fake key import + if service_type == "provider_info": +- _client_service.upstream_get("attribute", "keyjar").import_jwks( +- _server_endpoint.upstream_get("attribute", "keyjar").export_jwks(), +- issuer_id=_server_endpoint.upstream_get("attribute", "issuer"), +- ) ++ _keyjar = _client_service.upstream_get("attribute", "keyjar") ++ _keyjar = import_jwks(_keyjar, ++ _server_endpoint.upstream_get("attribute", "keyjar").export_jwks(), ++ _server_endpoint.upstream_get("attribute", "issuer")) ++ + return areq, resp + + def process_setup(self, token=None, scope=None): +@@ -252,10 +257,13 @@ class TestFlow(object): + if scope: + _scope = scope + else: +- _scope = ["openid"] +- +- if token and list(token.keys())[0] == "refresh_token": +- _scope = ["openid", "offline_access"] ++ _scope = _context.claims.get_usage("scope", None) ++ if not _scope: ++ if token: ++ if isinstance(token, list) and list(token.keys())[0] == "refresh_token": ++ _scope = ["openid", "offline_access"] ++ else: ++ _scope = ["openid"] + + req_args["scope"] = _scope + +@@ -281,13 +289,11 @@ class TestFlow(object): + Test that token exchange requests work correctly + """ + +- resp, _state, _scope = self.process_setup( +- token="access_token", +- scope=["openid", "profile", "email", "address", "phone", "offline_access"], +- ) ++ resp, _state, _scope = self.process_setup(token="access_token") + + # The User Info request + + _request, resp = self.do_query("userinfo", "userinfo", {}, _state) + + assert resp ++ assert "given_name" in resp +diff --git a/tests/test_y_actor_01.py b/tests/test_y_actor_01.py +deleted file mode 100644 +index e69de29..0000000 +diff --git a/tests/x_test_ciba_01_backchannel_auth.py b/tests/x_test_ciba_01_backchannel_auth.py +index 62d79ac..bc4f67f 100644 +--- a/tests/x_test_ciba_01_backchannel_auth.py ++++ b/tests/x_test_ciba_01_backchannel_auth.py +@@ -487,133 +487,133 @@ CLI_KEY = init_key_jar( + ) + + +-class TestBCAEndpointService(object): +- @pytest.fixture(autouse=True) +- def create_endpoint(self): +- self.ciba = {"self.server": self._create_self.server(), "client": self._create_ciba_client()} +- +- def _create_self.server(self): +- self.server = Server(OPConfiguration(SERVER_CONF, base_path=BASEDIR)) +- context = self.server.context +- context.cdb["client_1"] = { +- "client_secret": "hemligt", +- "redirect_uris": [("https://example.com/cb", None)], +- "client_salt": "salted", +- "token_endpoint_auth_method": "client_secret_post", +- "response_types": ["code", "token", "code id_token", "id_token"], +- } +- +- client_keyjar = build_keyjar(KEYDEFS) +- # Add self.servers keys +- client_keyjar.import_jwks(self.server.keyjar.export_jwks(), ISSUER) +- # The only own key the client has a this point +- client_keyjar.add_symmetric("", CLIENT_SECRET, ["sig"]) +- # Need to add the client_secret as a symmetric key bound to the client_id +- self.server.keyjar.add_symmetric(CLIENT_ID, CLIENT_SECRET, ["sig"]) +- self.server.keyjar.import_jwks(client_keyjar.export_jwks(), CLIENT_ID) +- +- self.server.context.cdb = {CLIENT_ID: {"client_secret": CLIENT_SECRET}} +- # login_hint +- self.server.context.login_hint_lookup = init_service( +- {"class": "idpyoidc.self.server.login_hint.LoginHintLookup"}, None +- ) +- # userinfo +- _userinfo = init_user_info( +- { +- "class": "idpyoidc.self.server.user_info.UserInfo", +- "kwargs": {"db_file": full_path("users.json")}, +- }, +- "", +- ) +- self.server.context.login_hint_lookup.userinfo = _userinfo +- return self.server +- +- def _create_ciba_client(self): +- config = { +- "client_id": CLIENT_ID, +- "client_secret": CLIENT_SECRET, +- "redirect_uris": ["https://example.com/cb"], +- "services": { +- "client_notification": { +- "class": "idpyoidc.client.oidc.backchannel_authentication.ClientNotification", +- "kwargs": {"conf": {"default_authn_method": "client_notification_authn"}}, +- }, +- }, +- "client_authn_methods": { +- "client_notification_authn": { +- 'class': "idpyoidc.client.oidc.backchannel_authentication.ClientNotificationAuthn" +- } +- }, +- } +- +- client = Client(keyjar=CLI_KEY, config=config, services=DEFAULT_OAUTH2_SERVICES) +- +- client.upstream_get("context").provider_info = { +- "client_notification_endpoint": "https://example.com/notify", +- } +- +- return client +- +- def _create_session(self, user_id, auth_req, sub_type="public", sector_identifier=""): +- if sector_identifier: +- authz_req = auth_req.copy() +- authz_req["sector_identifier_uri"] = sector_identifier +- else: +- authz_req = auth_req +- client_id = authz_req["client_id"] +- ae = create_authn_event(user_id) +- _session_manager = self.ciba["self.server"].context.session_manager +- return _session_manager.create_session( +- ae, authz_req, user_id, client_id=client_id, sub_type=sub_type +- ) +- +- def test_client_notification(self): +- _keyjar = self.ciba["self.server"].context.keyjar +- _jwt = JWT(_keyjar, iss=CLIENT_ID, sign_alg="HS256") +- _jwt.with_jti = True +- _assertion = _jwt.pack({"aud": [ISSUER]}) +- +- request = { +- "client_assertion": _assertion, +- "client_assertion_type": JWT_BEARER, +- "scope": "openid email example-scope", +- "client_notification_token": "8d67dc78-7faa-4d41-aabd-67707b374255", +- "binding_message": "W4SCT", +- "login_hint": "mail:diana@example.org", +- } +- +- _authn_endpoint = self.ciba["self.server"].upstream_get("endpoint", "backchannel_authentication") +- +- req = AuthenticationRequest(**request) +- req = _authn_endpoint.parse_request(req.to_urlencoded()) +- _info = _authn_endpoint.process_request(req) +- assert _info +- +- _session_manager = self.ciba["self.server"].context.session_manager +- sid = _session_manager.auth_req_id_map[_info["response_args"]["auth_req_id"]] +- _user_id, _client_id, _grant_id = _session_manager.decrypt_session_id(sid) +- +- # Some time passes and the client authentication is successfully performed +- # The interaction with the authentication device is not shown +- session_id_2 = self._create_session(_user_id, req) +- +- # Now it's time to send a client notification +- req_args = { +- "auth_req_id": _info["response_args"]["auth_req_id"], +- "client_notification_token": request["client_notification_token"], +- } +- +- _service = self.ciba["client"].upstream_get("service", "client_notification") +- _req_param = _service.get_request_parameters(request_args=req_args) +- assert _req_param +- assert isinstance(_req_param["request"], NotificationRequest) +- assert set(_req_param.keys()) == {"method", "request", "url", "body", "headers"} +- assert _req_param["method"] == "POST" +- # This is the client's notification endpoint +- assert ( +- _req_param["url"] +- == self.ciba["client"] +- .upstream_get("context") +- .provider_info["client_notification_endpoint"] +- ) +- assert set(_req_param["request"].keys()) == {"auth_req_id", "client_notification_token"} ++# class TestBCAEndpointServi ce(object): ++# @pytest.fixture(autouse=True) ++# def create_endpoint(self): ++# self.ciba = {"self.server": self._create_self.server(), "client": self._create_ciba_client()} ++# ++# def _create_self.server(self): ++# self.server = Server(OPConfiguration(SERVER_CONF, base_path=BASEDIR)) ++# context = self.server.context ++# context.cdb["client_1"] = { ++# "client_secret": "hemligt", ++# "redirect_uris": [("https://example.com/cb", None)], ++# "client_salt": "salted", ++# "token_endpoint_auth_method": "client_secret_post", ++# "response_types": ["code", "token", "code id_token", "id_token"], ++# } ++# ++# client_keyjar = build_keyjar(KEYDEFS) ++# # Add self.servers keys ++# client_keyjar.import_jwks(self.server.keyjar.export_jwks(), ISSUER) ++# # The only own key the client has a this point ++# client_keyjar.add_symmetric("", CLIENT_SECRET, ["sig"]) ++# # Need to add the client_secret as a symmetric key bound to the client_id ++# self.server.keyjar.add_symmetric(CLIENT_ID, CLIENT_SECRET, ["sig"]) ++# self.server.keyjar.import_jwks(client_keyjar.export_jwks(), CLIENT_ID) ++# ++# self.server.context.cdb = {CLIENT_ID: {"client_secret": CLIENT_SECRET}} ++# # login_hint ++# self.server.context.login_hint_lookup = init_service( ++# {"class": "idpyoidc.self.server.login_hint.LoginHintLookup"}, None ++# ) ++# # userinfo ++# _userinfo = init_user_info( ++# { ++# "class": "idpyoidc.self.server.user_info.UserInfo", ++# "kwargs": {"db_file": full_path("users.json")}, ++# }, ++# "", ++# ) ++# self.server.context.login_hint_lookup.userinfo = _userinfo ++# return self.server ++# ++# def _create_ciba_client(self): ++# config = { ++# "client_id": CLIENT_ID, ++# "client_secret": CLIENT_SECRET, ++# "redirect_uris": ["https://example.com/cb"], ++# "services": { ++# "client_notification": { ++# "class": "idpyoidc.client.oidc.backchannel_authentication.ClientNotification", ++# "kwargs": {"conf": {"default_authn_method": "client_notification_authn"}}, ++# }, ++# }, ++# "client_authn_methods": { ++# "client_notification_authn": { ++# 'class': "idpyoidc.client.oidc.backchannel_authentication.ClientNotificationAuthn" ++# } ++# }, ++# } ++# ++# client = Client(keyjar=CLI_KEY, config=config, services=DEFAULT_OAUTH2_SERVICES) ++# ++# client.upstream_get("context").provider_info = { ++# "client_notification_endpoint": "https://example.com/notify", ++# } ++# ++# return client ++# ++# def _create_session(self, user_id, auth_req, sub_type="public", sector_identifier=""): ++# if sector_identifier: ++# authz_req = auth_req.copy() ++# authz_req["sector_identifier_uri"] = sector_identifier ++# else: ++# authz_req = auth_req ++# client_id = authz_req["client_id"] ++# ae = create_authn_event(user_id) ++# _session_manager = self.ciba["self.server"].context.session_manager ++# return _session_manager.create_session( ++# ae, authz_req, user_id, client_id=client_id, sub_type=sub_type ++# ) ++# ++# def test_client_notification(self): ++# _keyjar = self.ciba["self.server"].context.keyjar ++# _jwt = JWT(_keyjar, iss=CLIENT_ID, sign_alg="HS256") ++# _jwt.with_jti = True ++# _assertion = _jwt.pack({"aud": [ISSUER]}) ++# ++# request = { ++# "client_assertion": _assertion, ++# "client_assertion_type": JWT_BEARER, ++# "scope": "openid email example-scope", ++# "client_notification_token": "8d67dc78-7faa-4d41-aabd-67707b374255", ++# "binding_message": "W4SCT", ++# "login_hint": "mail:diana@example.org", ++# } ++# ++# _authn_endpoint = self.ciba["self.server"].upstream_get("endpoint", "backchannel_authentication") ++# ++# req = AuthenticationRequest(**request) ++# req = _authn_endpoint.parse_request(req.to_urlencoded()) ++# _info = _authn_endpoint.process_request(req) ++# assert _info ++# ++# _session_manager = self.ciba["self.server"].context.session_manager ++# sid = _session_manager.auth_req_id_map[_info["response_args"]["auth_req_id"]] ++# _user_id, _client_id, _grant_id = _session_manager.decrypt_session_id(sid) ++# ++# # Some time passes and the client authentication is successfully performed ++# # The interaction with the authentication device is not shown ++# session_id_2 = self._create_session(_user_id, req) ++# ++# # Now it's time to send a client notification ++# req_args = { ++# "auth_req_id": _info["response_args"]["auth_req_id"], ++# "client_notification_token": request["client_notification_token"], ++# } ++# ++# _service = self.ciba["client"].upstream_get("service", "client_notification") ++# _req_param = _service.get_request_parameters(request_args=req_args) ++# assert _req_param ++# assert isinstance(_req_param["request"], NotificationRequest) ++# assert set(_req_param.keys()) == {"method", "request", "url", "body", "headers"} ++# assert _req_param["method"] == "POST" ++# # This is the client's notification endpoint ++# assert ( ++# _req_param["url"] ++# == self.ciba["client"] ++# .upstream_get("context") ++# .provider_info["client_notification_endpoint"] ++# ) ++# assert set(_req_param["request"].keys()) == {"auth_req_id", "client_notification_token"} diff --git a/patch/transform.patch b/patch/transform.patch new file mode 100644 index 00000000..b79ae0f5 --- /dev/null +++ b/patch/transform.patch @@ -0,0 +1,60 @@ +diff --git a/src/idpyoidc/client/claims/transform.py b/src/idpyoidc/transform.py +similarity index 94% +rename from src/idpyoidc/client/claims/transform.py +rename to src/idpyoidc/transform.py +index 1ca40c6..3834006 100644 +--- a/src/idpyoidc/client/claims/transform.py ++++ b/src/idpyoidc/transform.py +@@ -51,10 +51,10 @@ REQUEST2REGISTER = { + + + def supported_to_preferred( +- supported: dict, +- preference: dict, +- base_url: str, +- info: Optional[dict] = None, ++ supported: dict, ++ preference: dict, ++ base_url: str, ++ info: Optional[dict] = None, + ): + if info: # The provider info + for key, val in supported.items(): +@@ -83,7 +83,7 @@ def supported_to_preferred( + preference[key] = [x for x in val if x in _info_val] + else: + pass +- else: ++ elif val: + preference[key] = val + + # special case -> must have a request_uris value +@@ -148,7 +148,7 @@ def _intersection(a, b): + + + def preferred_to_registered( +- prefers: dict, supported: dict, registration_response: Optional[dict] = None ++ prefers: dict, supported: dict, registration_response: Optional[dict] = None + ): + """ + The claims with values that are returned from the OP is what goes unless (!!) +@@ -200,7 +200,7 @@ def preferred_to_registered( + # be a singleton or an array. So just add it as is. + registered[_reg_key] = val + +- logger.debug(f"Entity registered: {registered}") ++ logger.debug(f"preferred2registered: {registered}") + return registered + + +@@ -219,4 +219,10 @@ def create_registration_request(prefers: dict, supported: dict) -> dict: + continue + + _request[key] = array_or_singleton(spec, value) ++ ++ for key, val in prefers.items(): ++ if key not in RegistrationRequest.c_param.keys(): ++ if key not in REGISTER2PREFERRED.values(): ++ _request[key] = val ++ + return _request From ab12313f8029043c862854235ca3d27e2378a39e Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 09:09:53 +0100 Subject: [PATCH 03/21] Don't set charset in content_type. Metadata is more general than provider_info. Use metadata schema is available. --- src/idpyoidc/server/claims/oidc.py | 8 ++++++-- src/idpyoidc/server/endpoint.py | 12 ++++++------ src/idpyoidc/server/endpoint_context.py | 2 +- tests/test_08_transform.py | 12 +++++++----- tests/test_09_work_condition.py | 15 ++++++++++++--- tests/test_client_05_util.py | 2 +- tests/test_client_16_util.py | 2 +- tests/test_server_16_endpoint.py | 3 ++- ...est_server_22_oidc_provider_config_endpoint.py | 2 +- tests/test_server_31_oauth2_introspection.py | 2 +- tests/test_server_32_oidc_read_registration.py | 2 +- 11 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/idpyoidc/server/claims/oidc.py b/src/idpyoidc/server/claims/oidc.py index 6d5efd6a..91ea2154 100644 --- a/src/idpyoidc/server/claims/oidc.py +++ b/src/idpyoidc/server/claims/oidc.py @@ -1,6 +1,7 @@ from typing import Optional from idpyoidc import alg_info +from idpyoidc.message import Message from idpyoidc.message.oidc import ProviderConfigurationResponse from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.message.oidc import RegistrationResponse @@ -91,9 +92,12 @@ def verify_rules(self, supports): self.set_preference("id_token_encryption_alg_values_supported", []) self.set_preference("id_token_encryption_enc_values_supported", []) - def provider_info(self, supports): + def metadata(self, supports, schema: Optional[Message] = None): _info = {} - for key in ProviderConfigurationResponse.c_param.keys(): + if schema is None: + schema = ProviderConfigurationResponse + + for key in schema.c_param.keys(): _val = self.get_preference(key, supports.get(key, None)) if _val not in [None, []]: _info[key] = _val diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index f23f9955..6e41a6f7 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -416,14 +416,14 @@ def do_response( if self.response_placement == "body": if self.response_format == "json": if not content_type: - content_type = "application/json; charset=utf-8" + content_type = "application/json" if isinstance(_response, Message): resp = _response.to_json() else: resp = json.dumps(_response) elif self.response_format in ["jws", "jwe", "jose"]: if not content_type: - content_type = "application/jose; charset=utf-8" + content_type = "application/jose" resp = _response else: if not content_type: @@ -441,10 +441,10 @@ def do_response( else: fragment_enc = False - if fragment_enc: - resp = _response.request(kwargs["return_uri"], True) - else: - raise ValueError(f"Don't know where that is: '{self.response_placement}'") + resp = _response.request(kwargs["return_uri"], fragment_enc=fragment_enc) + else: + raise ValueError( + f"Don't know how to handle response_placement='{self.response_placement}'") if content_type: try: diff --git a/src/idpyoidc/server/endpoint_context.py b/src/idpyoidc/server/endpoint_context.py index dcfa3b69..b143fe2d 100755 --- a/src/idpyoidc/server/endpoint_context.py +++ b/src/idpyoidc/server/endpoint_context.py @@ -407,7 +407,7 @@ def supports(self): return res def set_provider_info(self): - _info = self.claims.provider_info(self.supports()) + _info = self.claims.metadata(self.supports()) _info.update({"issuer": self.issuer, "version": "3.0"}) for endp in self.upstream_get("endpoints").values(): diff --git a/tests/test_08_transform.py b/tests/test_08_transform.py index 71c83d9b..a3c2193f 100644 --- a/tests/test_08_transform.py +++ b/tests/test_08_transform.py @@ -4,9 +4,9 @@ from cryptojwt.utils import importer from idpyoidc.client.claims.oidc import Claims as OIDC_Claims -from idpyoidc.client.claims.transform import create_registration_request -from idpyoidc.client.claims.transform import preferred_to_registered -from idpyoidc.client.claims.transform import supported_to_preferred +from idpyoidc.transform import create_registration_request +from idpyoidc.transform import preferred_to_registered +from idpyoidc.transform import supported_to_preferred from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import ProviderConfigurationResponse from idpyoidc.message.oidc import RegistrationRequest @@ -248,8 +248,8 @@ def test_provider_info(self): assert set(claims.prefer.keys()) == { "application_type", "default_max_age", - "encrypt_request_object_supported", - "encrypt_userinfo_supported", + # "encrypt_request_object_supported", + # "encrypt_userinfo_supported", "id_token_encryption_alg_values_supported", "id_token_encryption_enc_values_supported", "id_token_signing_alg_values_supported", @@ -362,6 +362,8 @@ def test_registration_response(self): "client_name", "contacts", "default_max_age", + "encrypt_request_object_supported", + "encrypt_userinfo_supported", "id_token_signed_response_alg", "logo_uri", "redirect_uris", diff --git a/tests/test_09_work_condition.py b/tests/test_09_work_condition.py index 957d8570..abbd77d2 100644 --- a/tests/test_09_work_condition.py +++ b/tests/test_09_work_condition.py @@ -4,9 +4,9 @@ from cryptojwt.utils import importer from idpyoidc.client.claims.oidc import Claims -from idpyoidc.client.claims.transform import create_registration_request -from idpyoidc.client.claims.transform import preferred_to_registered -from idpyoidc.client.claims.transform import supported_to_preferred +from idpyoidc.transform import create_registration_request +from idpyoidc.transform import preferred_to_registered +from idpyoidc.transform import supported_to_preferred from idpyoidc.message.oidc import APPLICATION_TYPE_WEB KEYSPEC = [ @@ -179,9 +179,13 @@ def test_registration_response(self): assert set(registration_request.keys()) == { "application_type", + "client_id", "client_name", + "client_secret", "contacts", "default_max_age", + "encrypt_request_object_supported", + "encrypt_userinfo_supported", "id_token_signed_response_alg", "jwks", "logo_uri", @@ -318,8 +322,13 @@ def test_registration_response_consistence(self): assert set(registration_request.keys()) == { "application_type", "client_name", + "client_id", + "client_name", + "client_secret", "contacts", "default_max_age", + "encrypt_request_object_supported", + "encrypt_userinfo_supported", "id_token_signed_response_alg", "jwks", "logo_uri", diff --git a/tests/test_client_05_util.py b/tests/test_client_05_util.py index 3a22416a..d3088be6 100644 --- a/tests/test_client_05_util.py +++ b/tests/test_client_05_util.py @@ -141,7 +141,7 @@ def test_get_deserialization_method_json(): resp = FakeResponse("application/json") assert get_deserialization_method(resp) == "json" - resp = FakeResponse("application/json; charset=utf-8") + resp = FakeResponse("application/json") assert get_deserialization_method(resp) == "json" resp.headers["content-type"] = "application/jrd+json" diff --git a/tests/test_client_16_util.py b/tests/test_client_16_util.py index a09d65a5..0fdeede0 100644 --- a/tests/test_client_16_util.py +++ b/tests/test_client_16_util.py @@ -147,7 +147,7 @@ def test_get_deserialization_method_json(): resp = FakeResponse("application/json") assert get_deserialization_method(resp) == "json" - resp = FakeResponse("application/json; charset=utf-8") + resp = FakeResponse("application/json") assert get_deserialization_method(resp) == "json" resp.headers["content-type"] = "application/jrd+json" diff --git a/tests/test_server_16_endpoint.py b/tests/test_server_16_endpoint.py index 5a3b59de..7c023993 100755 --- a/tests/test_server_16_endpoint.py +++ b/tests/test_server_16_endpoint.py @@ -209,7 +209,7 @@ def test_do_response_response_msg_1(self): def test_do_response_placement_body(self): self.endpoint.response_placement = "body" info = self.endpoint.do_response(EXAMPLE_MSG) - assert ("Content-type", "application/json; charset=utf-8") in info["http_headers"] + assert ("Content-type", "application/json") in info["http_headers"] assert ( info["response"] == '{"name": "Doe, Jane", "given_name": "Jane", "family_name": ' '"Doe"}' @@ -217,6 +217,7 @@ def test_do_response_placement_body(self): def test_do_response_placement_url(self): self.endpoint.response_placement = "url" + self.endpoint.response_format = "urlencoded" info = self.endpoint.do_response(EXAMPLE_MSG, return_uri="https://example.org/cb") assert ("Content-type", "application/x-www-form-urlencoded") in info["http_headers"] assert ( diff --git a/tests/test_server_22_oidc_provider_config_endpoint.py b/tests/test_server_22_oidc_provider_config_endpoint.py index 7000d724..723a4a49 100755 --- a/tests/test_server_22_oidc_provider_config_endpoint.py +++ b/tests/test_server_22_oidc_provider_config_endpoint.py @@ -92,7 +92,7 @@ def test_do_response(self): assert _msg["token_endpoint"] == "https://example.com/token" assert _msg["jwks_uri"] == "https://example.com/static/jwks.json" assert "claims_supported" not in _msg # No default for this - assert ("Content-type", "application/json; charset=utf-8") in msg["http_headers"] + assert ("Content-type", "application/json") in msg["http_headers"] def test_scopes_supported(self, conf): scopes_supported = ["openid", "random", "profile"] diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py index bf283476..1ad30e1c 100644 --- a/tests/test_server_31_oauth2_introspection.py +++ b/tests/test_server_31_oauth2_introspection.py @@ -325,7 +325,7 @@ def test_do_response(self): assert isinstance(msg_info, dict) assert set(msg_info.keys()) == {"response", "http_headers"} assert msg_info["http_headers"] == [ - ("Content-type", "application/json; charset=utf-8"), + ("Content-type", "application/json"), ("Pragma", "no-cache"), ("Cache-Control", "no-store"), ] diff --git a/tests/test_server_32_oidc_read_registration.py b/tests/test_server_32_oidc_read_registration.py index e09bc5cd..2dea6413 100644 --- a/tests/test_server_32_oidc_read_registration.py +++ b/tests/test_server_32_oidc_read_registration.py @@ -160,4 +160,4 @@ def test_do_response(self): _endp_response = self.registration_api_endpoint.do_response(_info) assert set(_endp_response.keys()) == {"response", "http_headers"} - assert ("Content-type", "application/json; charset=utf-8") in _endp_response["http_headers"] + assert ("Content-type", "application/json") in _endp_response["http_headers"] From 44e8601274de229e49adca67ea4884b64a2a4e84 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 09:34:04 +0100 Subject: [PATCH 04/21] New method get_client_id_from_token in SessionManager New method get_metadata in EndpointContext --- src/idpyoidc/server/endpoint_context.py | 32 +++++++++++----- src/idpyoidc/server/session/grant.py | 2 +- src/idpyoidc/server/session/manager.py | 51 ++++++++++--------------- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/idpyoidc/server/endpoint_context.py b/src/idpyoidc/server/endpoint_context.py index b143fe2d..d5bcc51f 100755 --- a/src/idpyoidc/server/endpoint_context.py +++ b/src/idpyoidc/server/endpoint_context.py @@ -11,6 +11,7 @@ from requests import request from idpyoidc.context import OidcContext +from idpyoidc.message import Message from idpyoidc.server import authz from idpyoidc.server.claims import Claims from idpyoidc.server.claims.oauth2 import Claims as OAUTH2_Claims @@ -173,6 +174,7 @@ def __init__( self.token_args_methods = [] self.userinfo = None self.client_authn_method = {} + self.client_known_as = {} for param in [ "issuer", @@ -186,8 +188,6 @@ def __init__( except KeyError: pass - self.token_handler_args = get_token_handler_args(conf) - # session db self._sub_func = {} self.do_sub_func() @@ -240,9 +240,6 @@ def __init__( conf = conf.conf _supports = self.supports() self.keyjar = self.claims.load_conf(conf, supports=_supports, keyjar=keyjar) - self.provider_info = self.claims.metadata(_supports) - self.provider_info["issuer"] = self.issuer - self.provider_info.update(self._get_endpoint_info()) # INTERFACES @@ -256,17 +253,34 @@ def __init__( conf=conf, upstream_get=self.unit_get) + # default is to have session management + if self.conf.get("session_management", self.conf["conf"].get("session_management", True)): + self.token_handler_args = get_token_handler_args(self.conf) + + self.session_manager = SessionManager( + self.token_handler_args, + sub_func = self._sub_func, + conf = conf, + upstream_get = self.unit_get) + else: + self.session_manager = None + self.do_userinfo() # Must be done after userinfo self.setup_login_hint_lookup() - self.set_remember_token() + if self.session_manager: + self.set_remember_token() self.setup_client_authn_methods() - # _id_token_handler = self.session_manager.token_handler.handler.get("id_token") - # if _id_token_handler: - # self.provider_info.update(_id_token_handler.provider_info) + def get_metadata(self, supports: Optional[dict] = None, schema: Optional[Message] = None): + if supports is None: + supports = self.supports() + + _metadata = self.claims.metadata(supports, schema) + _metadata.update(self._get_endpoint_info()) + return _metadata def setup_authz(self): authz_spec = self.conf.get("authz") diff --git a/src/idpyoidc/server/session/grant.py b/src/idpyoidc/server/session/grant.py index d7ee7c39..d615a9b2 100644 --- a/src/idpyoidc/server/session/grant.py +++ b/src/idpyoidc/server/session/grant.py @@ -381,7 +381,7 @@ def mint_token( item.value = token_handler( session_id=session_id, usage_rules=usage_rules, **token_payload ) - + logger.debug(f"Minted token value: {item.value}") if based_on: based_on.used += 1 else: diff --git a/src/idpyoidc/server/session/manager.py b/src/idpyoidc/server/session/manager.py index ddd5a9dc..d209be60 100644 --- a/src/idpyoidc/server/session/manager.py +++ b/src/idpyoidc/server/session/manager.py @@ -1,10 +1,10 @@ import hashlib import logging import os -import uuid from typing import Callable from typing import List from typing import Optional +import uuid from idpyoidc.encrypter import default_crypt_config from idpyoidc.message.oauth2 import AuthorizationRequest @@ -13,16 +13,15 @@ from idpyoidc.server.exception import ConfigurationError from idpyoidc.server.session.grant_manager import GrantManager from idpyoidc.util import rndstr - from .database import Database -from ..exception import InvalidBranchID from .grant import Grant from .grant import SessionToken from .info import ClientSessionInfo from .info import UserSessionInfo -from ..token import handler +from ..exception import InvalidBranchID from ..token import UnknownToken from ..token import WrongTokenClass +from ..token import handler from ..token.handler import TokenHandler logger = logging.getLogger(__name__) @@ -84,6 +83,7 @@ def ephemeral_id(*args, **kwargs): class SessionManager(GrantManager): parameter = Database.parameter.copy() + # parameter.update({"salt": ""}) init_args = ["token_handler_args", "upstream_get"] @@ -437,30 +437,6 @@ def revoke_grant(self, session_id: str): """ self._revoke_tree(self.get_grant(session_id)) - # def grants( - # self, - # session_id: Optional[str] = "", - # user_id: Optional[str] = "", - # client_id: Optional[str] = "", - # ) -> List[Grant]: - # """ - # Find all grant connected to a user session - # - # :param client_id: - # :param user_id: - # :param session_id: A session identifier - # :return: A list of grants - # """ - # if session_id: - # user_id, client_id, _ = self.decrypt_session_id(session_id) - # elif user_id and client_id: - # pass - # else: - # raise AttributeError("Must have session_id or user_id and client_id") - # - # _csi = self.get([user_id, client_id]) - # return [self.get([user_id, client_id, gid]) for gid in _csi.subordinate] - def get_session_info( self, session_id: str, @@ -488,7 +464,7 @@ def get_session_info( # Log the exception if needed logging.error(f"InvalidBranchID error: {str(e)}") raise - + if authentication_event: res["authentication_event"] = res["grant"].authentication_event @@ -551,5 +527,18 @@ def encrypted_session_id(self, *args): def unpack_session_key(self, key): return self.unpack_branch_key(key) -# def create_session_manager(upstream_get, token_handler_args, sub_func=None, conf=None): -# return SessionManager(token_handler_args, sub_func=sub_func, conf=conf, upstream_get=upstream_get) + # def create_session_manager(upstream_get, token_handler_args, sub_func=None, conf=None): + # return SessionManager(token_handler_args, sub_func=sub_func, conf=conf, + # upstream_get=upstream_get) + def get_client_id_from_token(self, token_value: str, handler_key: Optional[str] = ""): + if handler_key: + _token_info = self.token_handler.handler[handler_key].info(token_value) + else: + _token_info = self.token_handler.info(token_value) + + sid = _token_info.get("sid") + _path = self.decrypt_branch_id(sid) + if len(_path) == 3: + return _path[1] + else: + return _path[-1] From de0901b65885c24786e747ec4ebe9d2e4a292e0d Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 09:47:42 +0100 Subject: [PATCH 05/21] Forgot to remove SessionManager initialization. --- src/idpyoidc/server/endpoint_context.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/idpyoidc/server/endpoint_context.py b/src/idpyoidc/server/endpoint_context.py index d5bcc51f..e70c2d4d 100755 --- a/src/idpyoidc/server/endpoint_context.py +++ b/src/idpyoidc/server/endpoint_context.py @@ -247,12 +247,6 @@ def __init__( self.setup_authentication() - self.session_manager = SessionManager( - self.token_handler_args, - sub_func=self._sub_func, - conf=conf, - upstream_get=self.unit_get) - # default is to have session management if self.conf.get("session_management", self.conf["conf"].get("session_management", True)): self.token_handler_args = get_token_handler_args(self.conf) From 6e5549e4491c32aab40472eb7ff340b1cdc8c11b Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 10:06:59 +0100 Subject: [PATCH 06/21] Added log lines Use key and password if static key handling is necessary. --- src/idpyoidc/server/token/__init__.py | 23 ++++++++++++++++++++--- src/idpyoidc/server/token/handler.py | 20 ++++++++++++++++---- src/idpyoidc/server/token/jwt_token.py | 8 +++++++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/idpyoidc/server/token/__init__.py b/src/idpyoidc/server/token/__init__.py index 8c92e562..42041e3d 100755 --- a/src/idpyoidc/server/token/__init__.py +++ b/src/idpyoidc/server/token/__init__.py @@ -105,23 +105,40 @@ def __call__( else: token_class = "authorization_code" + logger.debug(f"Mint {token_class}") + logger.debug(f"crypt.key: {self.crypt.key}") + _jwks = self.crypt_config.get('jwks', None) + logger.debug(f"crypt.jwks: {_jwks}") + if self.lifetime >= 0: exp = str(utc_time_sans_frac() + self.lifetime) else: - exp = "-1" # Live for ever + exp = "-1" # Live forever tmp = "" rnd = "" while rnd == tmp: # Don't use the same random value again rnd = rndstr(32) # Ultimate length multiple of 16 - return base64.b64encode( + _args = { + "rnd": rnd, + "token_class": token_class, + "session_id": session_id, + "exp": exp + } + logger.debug(f"Encrypt arguments: {_args}") + _value = base64.urlsafe_b64encode( self.crypt.encrypt(lv_pack(rnd, token_class, session_id, exp).encode()) ).decode("utf-8") + logger.debug(f"Token: {_value}") + return _value + def split_token(self, token): + logger.debug(f"split_token: {token}") + logger.debug(f"crypt key: {self.crypt.key}") try: - plain = self.crypt.decrypt(base64.b64decode(token)) + plain = self.crypt.decrypt(base64.urlsafe_b64decode(token)) except Exception as err: raise UnknownToken(err) # order: rnd, type, sid diff --git a/src/idpyoidc/server/token/handler.py b/src/idpyoidc/server/token/handler.py index 8fa90631..06f4bd3a 100755 --- a/src/idpyoidc/server/token/handler.py +++ b/src/idpyoidc/server/token/handler.py @@ -137,6 +137,9 @@ def default_token(spec): else: return False +def key_types(keys): + return [k["kid"] for k in keys] + JWKS_FILE = "private/token_jwks.json" @@ -192,10 +195,19 @@ def factory( ("token", token, "access_token"), ("refresh", refresh, "refresh_token"), ]: - if cnf is not None: - if default_token(cnf): - if kj: - _add_passwd(kj, cnf, cls) + if cnf is not None: # else just default + try: + _key_types = key_types( + cnf["kwargs"]["crypt_conf"]["kwargs"]["keys"]["key_defs"]) + except KeyError: # will fail on keys if it fails + pass + else: + if "key" in _key_types and "password" in _key_types: + raise ValueError("You have to chose one of key or password") + if "password" not in _key_types and "key" not in _key_types: + if kj: + _add_passwd(kj, cnf, cls) + logger.debug(f"init_token_handler: {cls}") args[attr] = init_token_handler(upstream_get, cnf, token_class_map[cls]) if id_token is not None: diff --git a/src/idpyoidc/server/token/jwt_token.py b/src/idpyoidc/server/token/jwt_token.py index abbfa97f..23a64dac 100644 --- a/src/idpyoidc/server/token/jwt_token.py +++ b/src/idpyoidc/server/token/jwt_token.py @@ -1,3 +1,4 @@ +import logging from typing import Callable from typing import Optional from typing import Union @@ -16,6 +17,7 @@ from .exception import UnknownToken from .exception import WrongTokenClass +logger = logging.getLogger(__name__) class JWTToken(Token): def __init__( @@ -89,8 +91,12 @@ def __call__( lifetime = usage_rules.get("expires_in") else: lifetime = self.lifetime + + _keyjar = self.upstream_get("attribute", "keyjar") + logger.info(f"Key owners in the keyjar: {_keyjar.owners()}") + signer = JWT( - key_jar=self.upstream_get("attribute", "keyjar"), + key_jar=_keyjar, iss=self.issuer, lifetime=lifetime, sign_alg=self.alg, From 9d18123681977767e13490b6fc7576e6852f0520 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 10:27:50 +0100 Subject: [PATCH 07/21] New storage module abfile_no_cache.py with an AbstractFileSystem that doe not cache any information. There should be a read/write list file storage class. --- src/idpyoidc/storage/abfile.py | 10 +- src/idpyoidc/storage/abfile_no_cache.py | 210 ++++++++++++++++++++++++ src/idpyoidc/storage/listfile.py | 43 +++++ 3 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 src/idpyoidc/storage/abfile_no_cache.py diff --git a/src/idpyoidc/storage/abfile.py b/src/idpyoidc/storage/abfile.py index 6257fe21..d1c088f8 100644 --- a/src/idpyoidc/storage/abfile.py +++ b/src/idpyoidc/storage/abfile.py @@ -191,7 +191,7 @@ def is_changed(self, item): else: return False else: - logger.error("Could not access {}".format(fname)) + logger.error(f"Not a file '{fname}'") raise KeyError(item) def _read_info(self, fname): @@ -239,6 +239,14 @@ def synch(self): else: self.fmtime[f] = mtime + _keys = self.storage.keys() + for f in _keys: + fname = os.path.join(self.fdir, f) + if os.path.isfile(fname): + pass + else: + del self.storage[f] + def items(self): """ Implements the dict.items() method diff --git a/src/idpyoidc/storage/abfile_no_cache.py b/src/idpyoidc/storage/abfile_no_cache.py new file mode 100644 index 00000000..7d1d5c3b --- /dev/null +++ b/src/idpyoidc/storage/abfile_no_cache.py @@ -0,0 +1,210 @@ +import logging +import os +import time +from typing import Optional + +from cryptojwt.utils import importer +from filelock import FileLock + +from idpyoidc.storage import DictType +from idpyoidc.util import PassThru +from idpyoidc.util import QPKey + +logger = logging.getLogger(__name__) + + +class AbstractFileSystemNoCache(DictType): + """ + FileSystem implements a simple file based database. + It has a dictionary like interface. + Each key maps one-to-one to a file on disc, where the content of the + file is the value. + ONLY goes one level deep. + Not directories in directories. + """ + + def __init__( + self, + fdir: Optional[str] = "", + key_conv: Optional[str] = "", + value_conv: Optional[str] = "", + read_only: Optional[bool] = False, + **kwargs + ): + """ + items = FileSystem( + { + 'fdir': fdir, + 'key_conv':{'to': quote_plus, 'from': unquote_plus}, + 'value_conv':{'to': keyjar_to_jwks, 'from': jwks_to_keyjar} + }) + + :param fdir: The root of the directory + :param key_conv: Converts to/from the key displayed by this class to + users of it to something that can be used as a file name. + The value of key_conv is a class that has the methods 'serialize'/'deserialize'. + :param value_conv: As with key_conv you can convert/translate + the value bound to a key in the database to something that can easily + be stored in a file. Like with key_conv the value of this parameter + is a class that has the methods 'serialize'/'deserialize'. + """ + super(AbstractFileSystemNoCache, self).__init__( + fdir=fdir, key_conv=key_conv, value_conv=value_conv + ) + + self.fdir = fdir + self.read_only = read_only + + if key_conv: + self.key_conv = importer(key_conv)() + else: + self.key_conv = QPKey() + + if value_conv: + self.value_conv = importer(value_conv)() + else: + self.value_conv = PassThru() + + if not os.path.isdir(self.fdir): + os.makedirs(self.fdir) + + def get(self, item, default=None): + try: + return self[item] + except KeyError: + return default + + def __getitem__(self, item): + """ + Return the value bound to an identifier. + + :param item: The identifier. + :return: + """ + _file_name = self.key_conv.serialize(item) + logger.debug(f'Read from "{_file_name}"') + return self._read_info(_file_name) + + def __setitem__(self, key, value): + """ + Binds a value to a specific key. If the file that the key maps to + does not exist it will be created. The content of the file will be + set to the value given. + + :param key: Identifier + :param value: Value that should be bound to the identifier. + :return: + """ + + if self.read_only: + return + + if not os.path.isdir(self.fdir): + os.makedirs(self.fdir, exist_ok=True) + + try: + _file_name = self.key_conv.serialize(key) + except KeyError: + _file_name = key + + fname = os.path.join(self.fdir, _file_name) + lock = FileLock(f"{fname}.lock") + with lock: + with open(fname, "w") as fp: + fp.write(self.value_conv.serialize(value)) + + logger.debug(f'Wrote to "{_file_name}"') + + def __delitem__(self, key): + if self.read_only: + return + + fname = os.path.join(self.fdir, key) + if fname.endswith(".lock"): + if os.path.isfile(fname): + os.unlink(fname) + else: + if os.path.isfile(fname): + lock = FileLock(f"{fname}.lock") + with lock: + os.unlink(fname) + os.unlink(f"{fname}.lock") + def _keys(self): + """ + Implements the dict.keys() method + """ + keys = [] + for f in os.listdir(self.fdir): + fname = os.path.join(self.fdir, f) + + if not os.path.isfile(fname): + continue + if fname.endswith(".lock"): + continue + + keys.append(f) + + return keys + + def keys(self): + return [self.key_conv.deserialize(k) for k in self._keys()] + + def _read_info(self, key): + file_name = os.path.join(self.fdir, key) + if os.path.isfile(file_name): + try: + lock = FileLock(f"{file_name}.lock") + with lock: + info = open(file_name, "r").read().strip() + lock.release() + return self.value_conv.deserialize(info) + except Exception as err: + logger.error(err) + raise + else: + _msg = f"No such file: '{file_name}'" + logger.error(_msg) + return None + + def items(self): + """ + Implements the dict.items() method + """ + for k in self._keys(): + v = self._read_info(k) + yield self.key_conv.deserialize(k), v + + def clear(self): + """ + Completely resets the database. This means that all information in + the local cache and on disc will be erased. + """ + if self.read_only: + return + + if not os.path.isdir(self.fdir): + os.makedirs(self.fdir, exist_ok=True) + return + + for f in os.listdir(self.fdir): + del self[f] + + def __contains__(self, item): + file_name = os.path.join(self.fdir, self.key_conv.serialize(item)) + if os.path.isfile(file_name): + return True + else: + return False + + def __iter__(self): + for k in self._keys(): + yield self.key_conv.deserialize(k) + + def __call__(self, *args, **kwargs): + return [self.key_conv.deserialize(k) for k in self._keys()] + + def __len__(self): + if not os.path.isdir(self.fdir): + return 0 + + return len(self._keys()) \ No newline at end of file diff --git a/src/idpyoidc/storage/listfile.py b/src/idpyoidc/storage/listfile.py index 77520de3..b2c2895f 100644 --- a/src/idpyoidc/storage/listfile.py +++ b/src/idpyoidc/storage/listfile.py @@ -111,6 +111,49 @@ def __getitem__(self, item): else: return None + def __len__(self): + _lst = self._read_info(self.file_name) + + if _lst is None or _lst == []: + return 0 + return len(set(_lst)) + + def _read_info(self, fname): + if os.path.isfile(fname): + try: + lock = FileLock(f"{fname}.lock") + + with lock: + fp = open(fname, "r") + info = [x.strip() for x in fp.readlines()] + lock.release() + return list(set(info)) + except Exception as err: + logger.error(err) + raise + else: + _msg = f"No such file: '{fname}'" + logger.error(_msg) + return None + + def __call__(self): + return self._read_info(self.file_name) + + def list(self): + return self._read_info(self.file_name) + +class ReadWriteListFile(object): + def __init__(self, file_name): + self.file_name = file_name + + if not os.path.exists(file_name): + fp = open(file_name, "x") + fp.close() + + def __contains__(self, item): + _lst = self._read_info(self.file_name) + return item in _lst + def __len__(self): _lst = self._read_info(self.file_name) if _lst is None or _lst == []: From 38c5d992efd836251cf542b98826d210095c977a Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 10:48:41 +0100 Subject: [PATCH 08/21] Added PushedAuthorizationResponse class --- src/idpyoidc/message/__init__.py | 2 +- src/idpyoidc/message/oauth2/__init__.py | 4 ++++ src/idpyoidc/message/oidc/__init__.py | 4 ++-- tests/test_14_read_only_list_file.py | 3 +-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/idpyoidc/message/__init__.py b/src/idpyoidc/message/__init__.py index 46d23440..67e56f84 100644 --- a/src/idpyoidc/message/__init__.py +++ b/src/idpyoidc/message/__init__.py @@ -388,7 +388,7 @@ def _add_value(self, skey, vtyp, key, val, _deser, null_allowed, sformat="urlenc else: self._dict[skey] = val else: - raise DecodeError(ERRTXT % (key, "type != %s" % vtype)) + raise DecodeError(ERRTXT % (key, f"type != {vtype}, val:{val}, type:{type(val)}")) else: if val is None: self._dict[skey] = None diff --git a/src/idpyoidc/message/oauth2/__init__.py b/src/idpyoidc/message/oauth2/__init__.py index 788fe8c5..105b2865 100644 --- a/src/idpyoidc/message/oauth2/__init__.py +++ b/src/idpyoidc/message/oauth2/__init__.py @@ -559,6 +559,10 @@ def verify(self, **kwargs): return True +class PushedAuthorizationResponse(ResponseMessage): + c_param = ResponseMessage.c_param.copy() + c_param.update({"request_uri": SINGLE_REQUIRED_STRING}) + class SecurityEventToken(Message): c_param = { diff --git a/src/idpyoidc/message/oidc/__init__.py b/src/idpyoidc/message/oidc/__init__.py index fc5d114c..4d1d626b 100644 --- a/src/idpyoidc/message/oidc/__init__.py +++ b/src/idpyoidc/message/oidc/__init__.py @@ -1025,7 +1025,7 @@ def verify(self, **kwargs): except KeyError: pass - if "iss" in kwargs and "iss" in self: + if "iss" in kwargs and kwargs["iss"] and "iss" in self: if kwargs["iss"] != self["iss"]: raise ValueError("Wrong issuer") @@ -1191,7 +1191,7 @@ def make_openid_request( :param request_object_signing_alg: Which signing algorithm to use :param recv: The intended receiver of the request :param with_jti: Whether a JTI should be included in the JWT. - :param lifetime: How long the JWT is expect to be live. + :param lifetime: How long the JWT is expected to be live. :return: JWT encoded OpenID request """ diff --git a/tests/test_14_read_only_list_file.py b/tests/test_14_read_only_list_file.py index 2abdf9e9..141501ef 100644 --- a/tests/test_14_read_only_list_file.py +++ b/tests/test_14_read_only_list_file.py @@ -25,5 +25,4 @@ def test_read_only_list_file(): # sleep(2) # assert _read_only.is_changed(FILE_NAME) is True - assert set(_read_only) == {"one", "two", "three"} - assert _read_only[-1] == "three" \ No newline at end of file + assert set(_read_only.list()) == {"one", "two", "three"} From 726f875f7e0ada5af97b758cb8e7ba9dddfc12e0 Mon Sep 17 00:00:00 2001 From: Roland Hedberg Date: Tue, 26 Nov 2024 13:48:49 +0100 Subject: [PATCH 09/21] Added another client authentication method RequestParam. There is a difference between client_id and entity_id. --- src/idpyoidc/client/client_auth.py | 11 ++++++++++- src/idpyoidc/client/entity.py | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/idpyoidc/client/client_auth.py b/src/idpyoidc/client/client_auth.py index a8830cd4..baf03d91 100755 --- a/src/idpyoidc/client/client_auth.py +++ b/src/idpyoidc/client/client_auth.py @@ -10,6 +10,7 @@ from cryptojwt.jws.utils import alg2keytype from cryptojwt.utils import importer +from idpyoidc.client.request_object import construct_request_parameter from idpyoidc.defaults import DEF_SIGN_ALG from idpyoidc.defaults import JWT_BEARER from idpyoidc.message import Message @@ -31,6 +32,7 @@ DEFAULT_ACCESS_TOKEN_TYPE = "Bearer" + class AuthnFailure(Exception): """Unspecified Authentication failure""" @@ -46,7 +48,7 @@ def assertion_jwt(client_id, keys, audience, algorithm, lifetime=600): :param client_id: The Client ID :param keys: Signing keys - :param audience: Who is the receivers for this assertion + :param audience: Who's the receivers for this assertion :param algorithm: Signing algorithm :param lifetime: The lifetime of the signed Json Web Token :return: A Signed Json Web Token @@ -628,6 +630,12 @@ def get_signing_key_from_keyjar(self, algorithm, keyjar): return keyjar.get_signing_key(alg2keytype(algorithm), "", alg=algorithm) +class RequestParam(ClientAuthnMethod): + def construct(self, request, service=None, http_args=None, **kwargs): + request_object = construct_request_parameter(service, request, **kwargs) + request["request"] = request_object + + # Map from client authentication identifiers to corresponding class CLIENT_AUTHN_METHOD = { "client_secret_basic": ClientSecretBasic, @@ -637,6 +645,7 @@ def get_signing_key_from_keyjar(self, algorithm, keyjar): "client_secret_jwt": ClientSecretJWT, "private_key_jwt": PrivateKeyJWT, # "client_notification_authn": ClientNotificationAuthn + "request_param": RequestParam } TYPE_METHOD = [(JWT_BEARER, JWSAuthnMethod)] diff --git a/src/idpyoidc/client/entity.py b/src/idpyoidc/client/entity.py index 197d5d73..2a1b0a67 100644 --- a/src/idpyoidc/client/entity.py +++ b/src/idpyoidc/client/entity.py @@ -103,8 +103,9 @@ def __init__( if config is None: config = {} + # Client ID is set through configuration or at registration _id = config.get("client_id") - self.client_id = self.entity_id = entity_id or config.get("entity_id", _id) + self.entity_id = entity_id or config.get("entity_id", _id) Unit.__init__( self, @@ -114,7 +115,7 @@ def __init__( httpc_params=httpc_params, config=config, key_conf=key_conf, - client_id=self.client_id, + client_id=_id, ) if services: From 4303e160de69375a03fb2a52b2f380e9e1f867cd Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 27 Nov 2024 09:38:08 +0100 Subject: [PATCH 10/21] Moved to use the wrappen keyjar methods. All about client_id being registered through configuration or not. Added a new function set_request_object() --- .../client/oauth2/stand_alone_client.py | 13 ++++++++++--- src/idpyoidc/client/oauth2/utils.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/idpyoidc/client/oauth2/stand_alone_client.py b/src/idpyoidc/client/oauth2/stand_alone_client.py index 8652f56d..c456176e 100644 --- a/src/idpyoidc/client/oauth2/stand_alone_client.py +++ b/src/idpyoidc/client/oauth2/stand_alone_client.py @@ -18,6 +18,8 @@ from idpyoidc.exception import MessageException from idpyoidc.exception import MissingRequiredAttribute from idpyoidc.exception import NotForMe +from idpyoidc.key_import import add_kb +from idpyoidc.key_import import import_jwks_from_file from idpyoidc.message import Message from idpyoidc.message.oauth2 import ResponseMessage from idpyoidc.message.oauth2 import is_error_message @@ -90,10 +92,10 @@ def do_provider_info( elif typ == "file": for kty, _name in _spec.items(): if kty == "jwks": - _kj.import_jwks_from_file(_name, _context.get("issuer")) + _kj = import_jwks_from_file(_kj, _name, _context.get("issuer")) elif kty == "rsa": # PEM file _kb = keybundle_from_local_file(_name, "der", ["sig"]) - _kj.add_kb(_context.get("issuer"), _kb) + _kj = add_kb(_kj, _context.get("issuer"), _kb) else: raise ValueError("Unknown provider JWKS type: {}".format(typ)) @@ -746,7 +748,12 @@ def load_registration_response(client, request_args=None): :param client: A :py:class:`idpyoidc.client.oidc.Client` instance """ - if not client.get_context().get_client_id(): + _client_id = getattr(client, "client_id", None) + if not _client_id: + _context = client.get_context() + _client_id = getattr(_context, "client_id", None) + + if not _client_id: try: response = client.do_request("registration", request_args=request_args) except KeyError: diff --git a/src/idpyoidc/client/oauth2/utils.py b/src/idpyoidc/client/oauth2/utils.py index 254e1bd2..819ff00a 100644 --- a/src/idpyoidc/client/oauth2/utils.py +++ b/src/idpyoidc/client/oauth2/utils.py @@ -2,6 +2,8 @@ from typing import Optional from typing import Union +from cryptojwt import JWT + from idpyoidc.client.defaults import DEFAULT_RESPONSE_MODE from idpyoidc.client.service import Service from idpyoidc.exception import MissingParameter @@ -99,3 +101,19 @@ def set_state_parameter(request_args=None, **kwargs): """Assigned a state value.""" request_args["state"] = get_state_parameter(request_args, kwargs) return request_args, {"state": request_args["state"]} + +def set_request_object(service, request_args): + # construct a signed request object + _context = service.upstream_get("context") + if _context.keyjar: + _jwt = JWT(key_jar=_context.keyjar) + else: + _jwt = JWT(key_jar=service.upstream_get("attribute", "keyjar")) + + if isinstance(request_args, Message): + _request_object = _jwt.pack(request_args.to_dict()) + else: + _request_object = _jwt.pack(request_args) + + # construct the message body + return _request_object \ No newline at end of file From e853da9f17422556b655a4d2c62db18c297030cc Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 27 Nov 2024 10:44:26 +0100 Subject: [PATCH 11/21] Distinguish between content type and deserialization method. (dpop.py) Use base64 encoded has instead of hexdigest. Several services may demand dpop headers. (jar.py) removed functions that was not used. construct_request_parameter() moved elsewhere. (par.py) With or without request object. --- src/idpyoidc/client/oauth2/__init__.py | 6 +- src/idpyoidc/client/oauth2/add_on/dpop.py | 27 +++---- src/idpyoidc/client/oauth2/add_on/jar.py | 93 +---------------------- src/idpyoidc/client/oauth2/add_on/par.py | 25 ++++-- 4 files changed, 36 insertions(+), 115 deletions(-) diff --git a/src/idpyoidc/client/oauth2/__init__.py b/src/idpyoidc/client/oauth2/__init__.py index 620608b0..0ecfd79f 100755 --- a/src/idpyoidc/client/oauth2/__init__.py +++ b/src/idpyoidc/client/oauth2/__init__.py @@ -6,6 +6,7 @@ from cryptojwt.key_jar import KeyJar + from idpyoidc.client.entity import Entity from idpyoidc.client.exception import ConfigurationError from idpyoidc.client.exception import OidcServiceError @@ -254,12 +255,13 @@ def parse_request_response(self, service, reqresp, response_body_type="", state= if reqresp.status_code in SUCCESSFUL: logger.debug('response_body_type: "{}"'.format(response_body_type)) + _content_type = reqresp.headers.get("content-type") _deser_method = get_deserialization_method(reqresp) - if _deser_method != response_body_type: + if _content_type != response_body_type: logger.warning( "Not the body type I expected: {} != {}".format( - _deser_method, response_body_type + _content_type, response_body_type ) ) if _deser_method in ["json", "jwt", "urlencoded"]: diff --git a/src/idpyoidc/client/oauth2/add_on/dpop.py b/src/idpyoidc/client/oauth2/add_on/dpop.py index d8a058ef..9b728837 100644 --- a/src/idpyoidc/client/oauth2/add_on/dpop.py +++ b/src/idpyoidc/client/oauth2/add_on/dpop.py @@ -1,8 +1,10 @@ +import base64 import logging import uuid from hashlib import sha256 from typing import Optional +from cryptojwt import as_unicode from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jws.jws import factory from cryptojwt.jws.jws import JWS @@ -149,7 +151,7 @@ def dpop_header( } if token: - header_dict["ath"] = sha256(token.encode("utf8")).hexdigest() + header_dict["ath"] = as_unicode(base64.urlsafe_b64encode(sha256(token.encode("utf8")).digest())) if nonce: header_dict["nonce"] = nonce @@ -168,14 +170,19 @@ def dpop_header( def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=None): """ - Add the necessary pieces to make pushed authorization happen. + Add the necessary pieces to make DPoP happen. :param services: A dictionary with all the services the client has access to. :param signing_algorithms: Allowed signing algorithms, there is no default algorithms + :param dpop_signing_alg_values_supported: Allowed signing algorithms, there is no default algorithms + :param with_dpop_header: If a services should add a DPoP header to a request """ - # Access token request should use DPoP header _service = services["accesstoken"] + if with_dpop_header is None: + with_dpop_header = ["accesstoken", "userinfo"] + _service = services[with_dpop_header[0]] + # Add to Context _context = _service.upstream_get("context") _algs_supported = [ alg for alg in dpop_signing_alg_values_supported if alg in get_signing_algs() @@ -186,20 +193,8 @@ def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=No } _context.set_preference("dpop_signing_alg_values_supported", _algs_supported) - _service.construct_extra_headers.append(dpop_header) - - # The same for userinfo requests - _userinfo_service = services.get("userinfo") - if _userinfo_service: - _userinfo_service.construct_extra_headers.append(dpop_header) - # To be backward compatible - if with_dpop_header is None: - with_dpop_header = ["userinfo"] - - # Add dpop HTTP header to these + # Add dpop HTTP header to requests by these services for _srv in with_dpop_header: - if _srv == "accesstoken": - continue _service = services.get(_srv) if _service: _service.construct_extra_headers.append(dpop_header) diff --git a/src/idpyoidc/client/oauth2/add_on/jar.py b/src/idpyoidc/client/oauth2/add_on/jar.py index 209c0627..ec8de2e3 100644 --- a/src/idpyoidc/client/oauth2/add_on/jar.py +++ b/src/idpyoidc/client/oauth2/add_on/jar.py @@ -1,11 +1,10 @@ import logging from typing import Optional +from idpyoidc.client.request_object import construct_request_parameter + from idpyoidc import alg_info from idpyoidc.client.oidc.utils import construct_request_uri -from idpyoidc.client.oidc.utils import request_object_encryption -from idpyoidc.message.oidc import make_openid_request -from idpyoidc.time_util import utc_time_sans_frac logger = logging.getLogger(__name__) @@ -34,94 +33,6 @@ def store_request_on_file(service, req, **kwargs): return _webname -def get_request_object_signing_alg(service, **kwargs): - alg = "" - for arg in ["request_object_signing_alg", "algorithm"]: - try: # Trumps everything - alg = kwargs[arg] - except KeyError: - pass - else: - break - - if not alg: - _context = service.upstream_get("context") - alg = _context.add_on["jar"].get("request_object_signing_alg") - if alg is None: - alg = "RS256" - return alg - - -def construct_request_parameter(service, req, audience=None, **kwargs): - """Construct a request parameter""" - alg = get_request_object_signing_alg(service, **kwargs) - kwargs["request_object_signing_alg"] = alg - - _context = service.upstream_get("context") - if "keys" not in kwargs and alg and alg != "none": - kwargs["keys"] = service.upstream_get("attribute", "keyjar") - - if alg == "none": - kwargs["keys"] = [] - - # This is the issuer of the JWT, that is me ! - _issuer = kwargs.get("issuer") - if _issuer is None: - kwargs["issuer"] = _context.get_client_id() - - if kwargs.get("recv") is None: - try: - kwargs["recv"] = _context.provider_info["issuer"] - except KeyError: - kwargs["recv"] = _context.issuer - - try: - del kwargs["service"] - except KeyError: - pass - - _jar_conf = _context.add_on["jar"] - expires_in = _jar_conf.get("expires_in", DEFAULT_EXPIRES_IN) - if expires_in: - req["exp"] = utc_time_sans_frac() + int(expires_in) - - if _jar_conf.get("with_jti", False): - kwargs["with_jti"] = True - - _enc_enc = _jar_conf.get("request_object_encryption_enc", "") - if _enc_enc: - kwargs["request_object_encryption_enc"] = _enc_enc - kwargs["request_object_encryption_alg"] = _jar_conf.get("request_object_encryption_alg") - - # Filter out only the arguments I want - _mor_args = { - k: kwargs[k] - for k in [ - "keys", - "issuer", - "request_object_signing_alg", - "recv", - "with_jti", - "lifetime", - ] - if k in kwargs - } - - if audience: - _mor_args["aud"] = audience - - _req_jwt = make_openid_request(req, **_mor_args) - - if "target" not in kwargs: - kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) - - # Should the request be encrypted - _req_jwte = request_object_encryption( - _req_jwt, _context, service.upstream_get("attribute", "keyjar"), **kwargs - ) - return _req_jwte - - def jar_post_construct(request_args, service, **kwargs): """ Modify the request arguments. diff --git a/src/idpyoidc/client/oauth2/add_on/par.py b/src/idpyoidc/client/oauth2/add_on/par.py index afa94058..353dc7b6 100644 --- a/src/idpyoidc/client/oauth2/add_on/par.py +++ b/src/idpyoidc/client/oauth2/add_on/par.py @@ -3,6 +3,8 @@ from cryptojwt.utils import importer from idpyoidc.client.client_auth import CLIENT_AUTHN_METHOD +from idpyoidc.client.oauth2.utils import set_request_object +from idpyoidc.client.service import Service from idpyoidc.message import Message from idpyoidc.message.oauth2 import JWTSecuredAuthorizationRequest from idpyoidc.server.util import execute @@ -13,7 +15,7 @@ HTTP_METHOD = "POST" -def push_authorization(request_args, service, **kwargs): +def push_authorization(request_args: Message, service: Service, **kwargs): """ :param request_args: All the request arguments as a AuthorizationRequest instance :param service: The service to which this post construct method is applied. @@ -50,17 +52,31 @@ def push_authorization(request_args, service, **kwargs): # construct the message body _body = request_args.to_urlencoded() + if isinstance(request_args, Message): + _required_params = request_args.to_dict() + else: + _required_params = request_args + + _add_request_object = kwargs.get("add_request_object", False) + if _add_request_object: + _required_params["request"] = set_request_object(service, request_args) + + _req = service.msg_type(**_required_params) + _body = _req.to_urlencoded() _http_client = method_args.get("http_client", None) if not _http_client: _http_client = service.upstream_get("unit").httpc _httpc_params = service.upstream_get("unit").httpc_params + _par_endpoint = kwargs.get("pushed_authorization_request_endpoint", None) + if not _par_endpoint: + _par_endpoint = _context.provider_info["pushed_authorization_request_endpoint"] # Send it to the Pushed Authorization Request Endpoint using POST resp = _http_client( method=HTTP_METHOD, - url=_context.provider_info["pushed_authorization_request_endpoint"], + url=_par_endpoint, data=_body, headers=_headers, **_httpc_params @@ -73,10 +89,7 @@ def push_authorization(request_args, service, **kwargs): _req[param] = request_args.get(param) request_args = _req else: - raise ConnectionError( - f"Could not connect to " - f'{_context.provider_info["pushed_authorization_request_endpoint"]}' - ) + raise ConnectionError(f"Could not connect to {_par_endpoint}") return request_args From f6b7bd2a23da31bd275a3925613e102d88cfeb35 Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 27 Nov 2024 10:52:14 +0100 Subject: [PATCH 12/21] No default response_modes_supported. Defined PushedAuthorization service. Used key jar wrapper. --- src/idpyoidc/client/oauth2/authorization.py | 2 +- .../client/oauth2/pushed_authorization.py | 89 +++++++++++++++++++ src/idpyoidc/client/oauth2/registration.py | 3 +- src/idpyoidc/metadata.py | 0 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/idpyoidc/client/oauth2/pushed_authorization.py delete mode 100644 src/idpyoidc/metadata.py diff --git a/src/idpyoidc/client/oauth2/authorization.py b/src/idpyoidc/client/oauth2/authorization.py index 9d85f1fd..04ae98d6 100644 --- a/src/idpyoidc/client/oauth2/authorization.py +++ b/src/idpyoidc/client/oauth2/authorization.py @@ -31,7 +31,7 @@ class Authorization(Service): _supports = { "response_types_supported": ["code"], - "response_modes_supported": ["query", "fragment"], + "grant_types": None } _callback_path = { diff --git a/src/idpyoidc/client/oauth2/pushed_authorization.py b/src/idpyoidc/client/oauth2/pushed_authorization.py new file mode 100644 index 00000000..20eb299d --- /dev/null +++ b/src/idpyoidc/client/oauth2/pushed_authorization.py @@ -0,0 +1,89 @@ +"""The service that talks to the OAuth2 Authorization endpoint.""" +import logging + +from idpyoidc.client.oauth2.utils import get_state_parameter +from idpyoidc.client.oauth2.utils import pre_construct_pick_redirect_uri +from idpyoidc.client.oauth2.utils import set_request_object +from idpyoidc.client.oauth2.utils import set_state_parameter +from idpyoidc.client.service import Service +from idpyoidc.exception import MissingParameter +from idpyoidc.message import oauth2 +from idpyoidc.message.oauth2 import ResponseMessage +from idpyoidc.time_util import time_sans_frac + +LOGGER = logging.getLogger(__name__) + + +class PushedAuthorization(Service): + """The service that talks to the OAuth2 Pushed Authorization endpoint.""" + + msg_type = oauth2.PushedAuthorizationRequest + response_cls = oauth2.PushedAuthorizationResponse + error_msg = ResponseMessage + endpoint_name = "pushed_authorization_request_endpoint" + service_name = "pushed_authorization" + response_body_type = "json" + http_method = "POST" + + _supports = { + "response_types_supported": ["code"], + "grant_types": None + } + + def __init__(self, upstream_get, conf=None): + Service.__init__(self, upstream_get, conf=conf) + self.pre_construct.extend([pre_construct_pick_redirect_uri, set_state_parameter]) + self.post_construct.append(self.store_auth_request) + + def add_(self, request_args=None, **kwargs): + _add_request_object = kwargs.get("add_request_object", False) + if _add_request_object: + request_args["request"] = set_request_object(self, request_args) + + def update_service_context(self, resp, key="", **kwargs): + if "expires_in" in resp: + resp["__expires_at"] = time_sans_frac() + int(resp["expires_in"]) + self.upstream_get("context").cstate.update(key, resp) + + def store_auth_request(self, request_args=None, **kwargs): + """Store the authorization request in the state DB.""" + _key = get_state_parameter(request_args, kwargs) + self.upstream_get("context").cstate.update(_key, request_args) + return request_args + + def gather_request_args(self, **kwargs): + ar_args = Service.gather_request_args(self, **kwargs) + + if "redirect_uri" not in ar_args: + try: + ar_args["redirect_uri"] = self.upstream_get("context").get_usage("redirect_uris")[0] + except (KeyError, AttributeError): + raise MissingParameter("redirect_uri") + + return ar_args + + def post_parse_response(self, response, **kwargs): + """ + Add scope claim to response, from the request, if not present in the + response + + :param response: The response + :param kwargs: Extra Keyword arguments + :return: A possibly augmented response + """ + + if "scope" not in response: + try: + _key = kwargs["state"] + except KeyError: + pass + else: + if _key: + item = self.upstream_get("context").cstate.get_set( + _key, message=oauth2.AuthorizationRequest + ) + try: + response["scope"] = item["scope"] + except KeyError: + pass + return response diff --git a/src/idpyoidc/client/oauth2/registration.py b/src/idpyoidc/client/oauth2/registration.py index 19da4982..ba2ecab0 100644 --- a/src/idpyoidc/client/oauth2/registration.py +++ b/src/idpyoidc/client/oauth2/registration.py @@ -4,6 +4,7 @@ from idpyoidc.client.entity import response_types_to_grant_types from idpyoidc.client.service import Service +from idpyoidc.key_import import store_under_other_id from idpyoidc.message import oauth2 from idpyoidc.message.oauth2 import ResponseMessage @@ -75,7 +76,7 @@ def update_service_context(self, resp, key="", **kwargs): _keyjar = self.upstream_get("attribute", "keyjar") if _keyjar: if _client_id not in _keyjar: - _keyjar.import_jwks(_keyjar.export_jwks(True, ""), issuer_id=_client_id) + _keyjar = store_under_other_id(_keyjar, "", _client_id, True) _client_secret = _context.get_usage("client_secret") if _client_secret: if not _keyjar: diff --git a/src/idpyoidc/metadata.py b/src/idpyoidc/metadata.py deleted file mode 100644 index e69de29b..00000000 From 8f1d08bb0abc5f3008bcd80870a36bbdfb7c29a9 Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 27 Nov 2024 14:39:25 +0100 Subject: [PATCH 13/21] Arguments can be Message instancesor dictionaries. Refactored. Moved construct_request_parameter and get_request_object_signing_alg to a module of their own. construct_request_uri was also moved. Fixed tests --- src/idpyoidc/client/oauth2/add_on/jar.py | 2 +- src/idpyoidc/client/oidc/authorization.py | 89 +----- src/idpyoidc/client/oidc/utils.py | 85 ------ src/idpyoidc/client/request_object.py | 141 +++++++++ src/idpyoidc/client/util.py | 29 +- src/idpyoidc/message/oidc/__init__.py | 4 +- .../server/oauth2/pushed_authorization.py | 4 +- tests/test_client_24_oic_utils.py | 4 +- tests/test_server_16_endpoint_context.py | 7 +- ...t_server_40_oauth2_pushed_authorization.py | 2 +- tests/test_tandem_oauth2_add_on.py | 6 +- tests/test_tandem_oauth2_par_service.py | 285 ++++++++++++++++++ 12 files changed, 477 insertions(+), 181 deletions(-) delete mode 100644 src/idpyoidc/client/oidc/utils.py create mode 100644 src/idpyoidc/client/request_object.py create mode 100644 tests/test_tandem_oauth2_par_service.py diff --git a/src/idpyoidc/client/oauth2/add_on/jar.py b/src/idpyoidc/client/oauth2/add_on/jar.py index ec8de2e3..3c2b83f7 100644 --- a/src/idpyoidc/client/oauth2/add_on/jar.py +++ b/src/idpyoidc/client/oauth2/add_on/jar.py @@ -4,7 +4,7 @@ from idpyoidc.client.request_object import construct_request_parameter from idpyoidc import alg_info -from idpyoidc.client.oidc.utils import construct_request_uri +from idpyoidc.client.util import construct_request_uri logger = logging.getLogger(__name__) diff --git a/src/idpyoidc/client/oidc/authorization.py b/src/idpyoidc/client/oidc/authorization.py index 73c56929..d1393726 100644 --- a/src/idpyoidc/client/oidc/authorization.py +++ b/src/idpyoidc/client/oidc/authorization.py @@ -7,18 +7,16 @@ from idpyoidc.client.oauth2 import authorization from idpyoidc.client.oauth2.utils import pre_construct_pick_redirect_uri from idpyoidc.client.oidc import IDT2REG -from idpyoidc.client.oidc.utils import construct_request_uri -from idpyoidc.client.oidc.utils import request_object_encryption +from idpyoidc.client.request_object import construct_request_parameter from idpyoidc.client.service_context import ServiceContext +from idpyoidc.client.util import construct_request_uri from idpyoidc.client.util import implicit_response_types from idpyoidc.exception import MissingRequiredAttribute from idpyoidc.message import Message from idpyoidc.message import oauth2 from idpyoidc.message import oidc -from idpyoidc.message.oidc import make_openid_request from idpyoidc.message.oidc import verified_claim_name from idpyoidc.time_util import time_sans_frac -from idpyoidc.time_util import utc_time_sans_frac from idpyoidc.util import rndstr __author__ = "Roland Hedberg" @@ -142,7 +140,7 @@ def oidc_pre_construct(self, request_args=None, post_args=None, **kwargs): elif "openid" not in request_args["scope"]: request_args["scope"].append("openid") - # 'code' and/or 'id_token' in response_type means an ID Roken + # 'code' and/or 'id_token' in response_type means an ID Token # will eventually be returned, hence the need for a nonce if "code" in _response_types or "id_token" in _response_types: if "nonce" not in request_args: @@ -173,24 +171,6 @@ def oidc_pre_construct(self, request_args=None, post_args=None, **kwargs): return request_args, post_args - def get_request_object_signing_alg(self, **kwargs): - alg = "" - for arg in ["request_object_signing_alg", "algorithm"]: - try: # Trumps everything - alg = kwargs[arg] - except KeyError: - pass - else: - break - - if not alg: - _context = self.upstream_get("context") - try: - alg = _context.claims.get_usage("request_object_signing_alg") - except KeyError: # Use default - alg = "RS256" - return alg - def store_request_on_file(self, req, **kwargs): """ Stores the request parameter in a file. @@ -212,63 +192,6 @@ def store_request_on_file(self, req, **kwargs): fid.close() return _webname - def construct_request_parameter( - self, req, request_param, audience=None, expires_in=0, **kwargs - ): - """Construct a request parameter""" - alg = self.get_request_object_signing_alg(**kwargs) - kwargs["request_object_signing_alg"] = alg - - _context = self.upstream_get("context") - if "keys" not in kwargs and alg and alg != "none": - kwargs["keys"] = self.upstream_get("attribute", "keyjar") - - if alg == "none": - kwargs["keys"] = [] - - # This is the issuer of the JWT, that is me ! - _issuer = kwargs.get("issuer") - if _issuer is None: - kwargs["issuer"] = _context.get_client_id() - - if kwargs.get("recv") is None: - try: - kwargs["recv"] = _context.provider_info["issuer"] - except KeyError: - kwargs["recv"] = _context.issuer - - try: - del kwargs["service"] - except KeyError: - pass - - if expires_in: - req["exp"] = utc_time_sans_frac() + int(expires_in) - - _mor_args = { - k: kwargs[k] - for k in [ - "keys", - "issuer", - "request_object_signing_alg", - "recv", - "with_jti", - "lifetime", - ] - if k in kwargs - } - - _req_jwt = make_openid_request(req, **_mor_args) - - if "target" not in kwargs: - kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) - - # Should the request be encrypted - _req_jwte = request_object_encryption( - _req_jwt, _context, self.upstream_get("attribute", "keyjar"), **kwargs - ) - return _req_jwte - def oidc_post_construct(self, req, **kwargs): """ Modify the request arguments. @@ -300,13 +223,15 @@ def oidc_post_construct(self, req, **kwargs): _request_param = "request" _req = None # just a flag + kwargs["req"] = req if _request_param == "request_uri": kwargs["base_path"] = _context.get("base_url") + "/" + "requests" kwargs["local_dir"] = _context.get_usage("requests_dir", "./requests") - _req = self.construct_request_parameter(req, _request_param, **kwargs) + _req = construct_request_parameter(**kwargs) + del kwargs["req"] req["request_uri"] = self.store_request_on_file(_req, **kwargs) elif _request_param == "request": - _req = self.construct_request_parameter(req, _request_param, **kwargs) + _req = construct_request_parameter(**kwargs) req["request"] = _req if _req: diff --git a/src/idpyoidc/client/oidc/utils.py b/src/idpyoidc/client/oidc/utils.py deleted file mode 100644 index 2b428feb..00000000 --- a/src/idpyoidc/client/oidc/utils.py +++ /dev/null @@ -1,85 +0,0 @@ -import os - -from cryptojwt.jwe.jwe import JWE -from cryptojwt.jwe.utils import alg2keytype - -from idpyoidc.exception import MissingRequiredAttribute -from idpyoidc.util import rndstr - - -def request_object_encryption(msg, service_context, keyjar, **kwargs): - """ - Created an encrypted JSON Web token with *msg* as body. - - :param msg: The mesaqg - :param service_context: - :param kwargs: - :return: - """ - try: - encalg = kwargs["request_object_encryption_alg"] - except KeyError: - try: - encalg = service_context.get_usage("request_object_encryption_alg") - except KeyError: - return msg - - if not encalg: - return msg - - try: - encenc = kwargs["request_object_encryption_enc"] - except KeyError: - try: - encenc = service_context.get_usage("request_object_encryption_enc") - except KeyError: - raise MissingRequiredAttribute("No request_object_encryption_enc specified") - - if not encenc: - raise MissingRequiredAttribute("No request_object_encryption_enc specified") - - _jwe = JWE(msg, alg=encalg, enc=encenc) - _kty = alg2keytype(encalg) - - try: - _kid = kwargs["enc_kid"] - except KeyError: - _kid = "" - - _target = kwargs.get("target", kwargs.get("recv", None)) - if _target is None: - raise MissingRequiredAttribute("No target specified") - - if _kid: - _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target, kid=_kid) - _jwe["kid"] = _kid - else: - _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target) - - return _jwe.encrypt(_keys) - - -def construct_request_uri(local_dir, base_path, **kwargs): - """ - Constructs a special redirect_uri to be used when communicating with - one OP. Each OP should get their own redirect_uris. - - :param local_dir: Local directory in which to place the file - :param base_path: Base URL to start with - :param kwargs: - :return: 2-tuple with (filename, url) - """ - _filedir = local_dir - if not os.path.isdir(_filedir): - os.makedirs(_filedir) - _webpath = base_path - _name = rndstr(10) + ".jwt" - filename = os.path.join(_filedir, _name) - while os.path.exists(filename): - _name = rndstr(10) - filename = os.path.join(_filedir, _name) - if _webpath.endswith("/"): - _webname = f"{_webpath}{_name}" - else: - _webname = f"{_webpath}/{_name}" - return filename, _webname diff --git a/src/idpyoidc/client/request_object.py b/src/idpyoidc/client/request_object.py new file mode 100644 index 00000000..35520639 --- /dev/null +++ b/src/idpyoidc/client/request_object.py @@ -0,0 +1,141 @@ +from typing import Optional +from typing import Union + +from cryptojwt.jwe.jwe import JWE +from cryptojwt.jwe.utils import alg2keytype +from cryptojwt.jwt import utc_time_sans_frac + +from idpyoidc.defaults import DEF_SIGN_ALG +from idpyoidc.exception import MissingRequiredAttribute +from idpyoidc.message import Message +from idpyoidc.message.oidc import make_openid_request + + +def request_object_encryption(msg, service_context, keyjar, **kwargs): + """ + Created an encrypted JSON Web token with *msg* as body. + + :param msg: The message + :param service_context: + :param kwargs: + :return: + """ + try: + encalg = kwargs["request_object_encryption_alg"] + except KeyError: + try: + encalg = service_context.get_usage("request_object_encryption_alg") + except KeyError: + return msg + + if not encalg: + return msg + + try: + encenc = kwargs["request_object_encryption_enc"] + except KeyError: + try: + encenc = service_context.get_usage("request_object_encryption_enc") + except KeyError: + raise MissingRequiredAttribute("No request_object_encryption_enc specified") + + if not encenc: + raise MissingRequiredAttribute("No request_object_encryption_enc specified") + + _jwe = JWE(msg, alg=encalg, enc=encenc) + _kty = alg2keytype(encalg) + + try: + _kid = kwargs["enc_kid"] + except KeyError: + _kid = "" + + _target = kwargs.get("target", kwargs.get("recv", None)) + if _target is None: + raise MissingRequiredAttribute("No target specified") + + if _kid: + _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target, kid=_kid) + _jwe["kid"] = _kid + else: + _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target) + + return _jwe.encrypt(_keys) + + +def get_request_object_signing_alg(self, **kwargs): + alg = "" + for arg in ["request_object_signing_alg", "algorithm"]: + try: # Trumps everything + alg = kwargs[arg] + except KeyError: + pass + else: + break + + if not alg: + _context = self.upstream_get("context") + try: + alg = _context.claims.get_usage("request_object_signing_alg") + except KeyError: # Use default + pass + + if not alg: + alg = DEF_SIGN_ALG["request_object"] + + return alg + + +def construct_request_parameter( + service, + req: Union[Message, dict], + expires_in: Optional[int] = 0, + **kwargs): + """Construct a request parameter""" + alg = get_request_object_signing_alg(service, **kwargs) + kwargs["request_object_signing_alg"] = alg + + _context = service.upstream_get("context") + if "keys" not in kwargs: + kwargs["keys"] = service.upstream_get("attribute", "keyjar") + + if alg == "none": + kwargs["keys"] = [] + + # This is the issuer of the JWT, that is me ! + _issuer = kwargs.get("issuer") + if _issuer is None: + kwargs["issuer"] = _context.get_client_id() + + if kwargs.get("recv") is None: + try: + kwargs["recv"] = _context.provider_info["issuer"] + except KeyError: + kwargs["recv"] = _context.issuer + + if expires_in: + req["exp"] = utc_time_sans_frac() + int(expires_in) + + _mor_args = { + k: kwargs[k] + for k in [ + "keys", + "issuer", + "request_object_signing_alg", + "recv", + "with_jti", + "lifetime", + ] + if k in kwargs + } + + _req_jwt = make_openid_request(req, **_mor_args) + + if "target" not in kwargs: + kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) + + # Should the request be encrypted + _req_jwte = request_object_encryption( + _req_jwt, _context, service.upstream_get("attribute", "keyjar"), **kwargs + ) + return _req_jwte diff --git a/src/idpyoidc/client/util.py b/src/idpyoidc/client/util.py index 03084d20..eb57e32e 100755 --- a/src/idpyoidc/client/util.py +++ b/src/idpyoidc/client/util.py @@ -1,5 +1,6 @@ """Utilities""" import logging +import os import secrets from http.cookiejar import Cookie from http.cookiejar import http2time @@ -14,9 +15,9 @@ from idpyoidc.defaults import BASECHR from idpyoidc.exception import UnSupported from idpyoidc.util import importer - from .exception import TimeFormatError from .exception import WrongContentType +from ..util import rndstr logger = logging.getLogger(__name__) @@ -330,3 +331,29 @@ def implicit_response_types(a): def get_uri(base_url, path, hex): return f"{base_url}/{path}/{hex}" + + +def construct_request_uri(local_dir, base_path, **kwargs): + """ + Constructs a special redirect_uri to be used when communicating with + one OP. Each OP should get their own redirect_uris. + + :param local_dir: Local directory in which to place the file + :param base_path: Base URL to start with + :param kwargs: + :return: 2-tuple with (filename, url) + """ + _filedir = local_dir + if not os.path.isdir(_filedir): + os.makedirs(_filedir) + _webpath = base_path + _name = rndstr(10) + ".jwt" + filename = os.path.join(_filedir, _name) + while os.path.exists(filename): + _name = rndstr(10) + filename = os.path.join(_filedir, _name) + if _webpath.endswith("/"): + _webname = f"{_webpath}{_name}" + else: + _webname = f"{_webpath}/{_name}" + return filename, _webname diff --git a/src/idpyoidc/message/oidc/__init__.py b/src/idpyoidc/message/oidc/__init__.py index 4d1d626b..8eeb2954 100644 --- a/src/idpyoidc/message/oidc/__init__.py +++ b/src/idpyoidc/message/oidc/__init__.py @@ -1200,7 +1200,9 @@ def make_openid_request( _jwt.with_jti = True if lifetime: _jwt.lifetime = lifetime - return _jwt.pack(arq.to_dict(), owner=issuer, recv=recv) + if isinstance(arq, Message): + arq = arq.to_dict() + return _jwt.pack(arq, owner=issuer, recv=recv) def claims_match(value, claimspec): diff --git a/src/idpyoidc/server/oauth2/pushed_authorization.py b/src/idpyoidc/server/oauth2/pushed_authorization.py index 693b073f..0f54373d 100644 --- a/src/idpyoidc/server/oauth2/pushed_authorization.py +++ b/src/idpyoidc/server/oauth2/pushed_authorization.py @@ -40,11 +40,11 @@ def process_request(self, request: Optional[Union[Message, str]] = None, **kwarg _request.verify(keyjar=self.upstream_get("attribute", "keyjar")) - _urn = "urn:uuid:{}".format(uuid.uuid4()) + _urn = f"urn:uuid:{uuid.uuid4()}" # Store the parsed and verified request self.upstream_get("context").par_db[_urn] = _request return { - "http_response": {"request_uri": _urn, "expires_in": self.ttl}, + "response_args": {"request_uri": _urn, "expires_in": self.ttl}, "return_uri": _request["redirect_uri"], } diff --git a/tests/test_client_24_oic_utils.py b/tests/test_client_24_oic_utils.py index 4e799803..d6c42425 100644 --- a/tests/test_client_24_oic_utils.py +++ b/tests/test_client_24_oic_utils.py @@ -1,9 +1,9 @@ from cryptojwt.jwe.jwe import factory from cryptojwt.key_jar import build_keyjar -from idpyoidc.client.oidc.utils import construct_request_uri -from idpyoidc.client.oidc.utils import request_object_encryption +from idpyoidc.client.request_object import request_object_encryption from idpyoidc.client.service_context import ServiceContext +from idpyoidc.client.util import construct_request_uri from idpyoidc.message.oidc import AuthorizationRequest KEYSPEC = [ diff --git a/tests/test_server_16_endpoint_context.py b/tests/test_server_16_endpoint_context.py index f96b676c..4cb390a3 100644 --- a/tests/test_server_16_endpoint_context.py +++ b/tests/test_server_16_endpoint_context.py @@ -5,17 +5,13 @@ from cryptojwt.key_jar import build_keyjar from idpyoidc import alg_info -from idpyoidc import metadata from idpyoidc.server import OPConfiguration from idpyoidc.server import Server from idpyoidc.server.endpoint import Endpoint -from idpyoidc.server.exception import OidcEndpointError from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD -from idpyoidc.server.util import allow_refresh_token - from . import CRYPT_CONFIG -from . import SESSION_PARAMS from . import full_path +from . import SESSION_PARAMS KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, @@ -83,6 +79,7 @@ class Endpoint_1(Endpoint): class TestEndpointContext: + @pytest.fixture(autouse=True) def create_endpoint_context(self): server = Server(conf) diff --git a/tests/test_server_40_oauth2_pushed_authorization.py b/tests/test_server_40_oauth2_pushed_authorization.py index 4d7ea6da..7cf6bb38 100644 --- a/tests/test_server_40_oauth2_pushed_authorization.py +++ b/tests/test_server_40_oauth2_pushed_authorization.py @@ -251,7 +251,7 @@ def test_pushed_auth_urlencoded_process(self): # And now for the authorization request with the OP provided request_uri - _msg["request_uri"] = _resp["http_response"]["request_uri"] + _msg["request_uri"] = _resp["response_args"]["request_uri"] for parameter in ["code_challenge", "code_challenge_method"]: del _msg[parameter] diff --git a/tests/test_tandem_oauth2_add_on.py b/tests/test_tandem_oauth2_add_on.py index a3776fc8..58abb2d2 100644 --- a/tests/test_tandem_oauth2_add_on.py +++ b/tests/test_tandem_oauth2_add_on.py @@ -3,6 +3,7 @@ from typing import List from cryptojwt.key_jar import build_keyjar +from idpyoidc.key_import import store_under_other_id from idpyoidc.client.oauth2 import Client from idpyoidc.message.oauth2 import is_error_message @@ -324,10 +325,13 @@ def test_jar(): }, } + _keyjar = build_keyjar(KEYDEFS) + _keyjar = store_under_other_id(_keyjar, "", client_config["client_id"], True) + client = Client( client_type="oauth2", config=client_config, - keyjar=build_keyjar(KEYDEFS), + keyjar=_keyjar, services=_OAUTH2_SERVICES, ) diff --git a/tests/test_tandem_oauth2_par_service.py b/tests/test_tandem_oauth2_par_service.py new file mode 100644 index 00000000..9630a55a --- /dev/null +++ b/tests/test_tandem_oauth2_par_service.py @@ -0,0 +1,285 @@ +import json +import os + +import pytest +from cryptojwt.key_jar import build_keyjar + +from idpyoidc.client.oauth2 import Client +from idpyoidc.key_import import import_jwks +from idpyoidc.message.oauth2 import is_error_message +from idpyoidc.message.oidc import AccessTokenRequest +from idpyoidc.message.oidc import AuthorizationRequest +from idpyoidc.message.oidc import RefreshAccessTokenRequest +from idpyoidc.server import Server +from idpyoidc.server.authz import AuthzHandling +from idpyoidc.server.client_authn import verify_client +from idpyoidc.server.configure import ASConfiguration +from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD +from idpyoidc.server.user_info import UserInfo +from idpyoidc.util import rndstr +from tests import CRYPT_CONFIG +from tests import SESSION_PARAMS + +KEYDEFS = [ + {"type": "RSA", "key": "", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, +] + +CLIENT_KEYJAR = build_keyjar(KEYDEFS) + +COOKIE_KEYDEFS = [ + {"type": "oct", "kid": "sig", "use": ["sig"]}, + {"type": "oct", "kid": "enc", "use": ["enc"]}, +] + +AUTH_REQ = AuthorizationRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + scope=["openid"], + state="STATE", + response_type="code", +) + +TOKEN_REQ = AccessTokenRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + state="STATE", + grant_type="authorization_code", + client_secret="hemligt", +) + +REFRESH_TOKEN_REQ = RefreshAccessTokenRequest( + grant_type="refresh_token", client_id="https://example.com/", client_secret="hemligt" +) + +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 = UserInfo(json.loads(open(full_path("users.json")).read())) + +_OAUTH2_SERVICES = { + "metadata": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"}, + "authorization": {"class": "idpyoidc.client.oauth2.authorization.Authorization"}, + "pushed_authorization": {"class": "idpyoidc.client.oauth2.pushed_authorization.PushedAuthorization"}, + "access_token": {"class": "idpyoidc.client.oauth2.access_token.AccessToken"}, + "resource": {"class": "idpyoidc.client.oauth2.resource.Resource"}, +} + + +class TestFlow(object): + + @pytest.fixture(autouse=True) + def create_entities(self): + server_conf = { + "issuer": "https://example.com/", + "httpc_params": {"verify": False, "timeout": 1}, + "subject_types_supported": ["public", "pairwise", "ephemeral"], + "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, + "endpoint": { + "metadata": { + "path": ".well-known/oauth-authorization-server", + "class": "idpyoidc.server.oauth2.server_metadata.ServerMetadata", + "kwargs": {}, + }, + "authorization": { + "path": "authorization", + "class": "idpyoidc.server.oauth2.authorization.Authorization", + "kwargs": {}, + }, + "pushed_authorization": { + "path": "par", + "class": "idpyoidc.server.oauth2.pushed_authorization.PushedAuthorization", + "kwargs": {}, + }, + "token": { + "path": "token", + "class": "idpyoidc.server.oauth2.token.Token", + "kwargs": {}, + }, + }, + "authentication": { + "anon": { + "acr": INTERNETPROTOCOLPASSWORD, + "class": "idpyoidc.server.user_authn.user.NoAuthn", + "kwargs": {"user": "diana"}, + } + }, + "userinfo": {"class": UserInfo, "kwargs": {"db": {}}}, + "client_authn": verify_client, + "authz": { + "class": AuthzHandling, + "kwargs": { + "grant_config": { + "usage_rules": { + "authorization_code": { + "supports_minting": ["access_token", "refresh_token"], + "max_usage": 1, + }, + "access_token": { + "supports_minting": ["access_token", "refresh_token"], + "expires_in": 600, + }, + "refresh_token": { + "supports_minting": ["access_token"], + "audience": ["https://example.com", "https://example2.com"], + "expires_in": 43200, + }, + }, + "expires_in": 43200, + } + }, + }, + "token_handler_args": { + "jwks_file": "private/token_jwks.json", + "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"], + }, + }, + }, + "session_params": SESSION_PARAMS, + } + self.server = Server(ASConfiguration(conf=server_conf, base_path=BASEDIR), cwd=BASEDIR) + + client_1_config = { + "issuer": server_conf["issuer"], + "client_secret": "hemligtlösenord", + "client_id": "client_1", + "redirect_uris": ["https://example.com/cb"], + "client_salt": "salted_peanuts_cooking", + "token_endpoint_auth_methods_supported": ["client_secret_post"], + "response_types_supported": ["code"], + } + client_services = _OAUTH2_SERVICES + self.client = Client( + client_type="oauth2", + config=client_1_config, + keyjar=build_keyjar(KEYDEFS), + services=_OAUTH2_SERVICES, + ) + + self.context = self.server.context + self.context.cdb["client_1"] = client_1_config + self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") + + self.context.set_provider_info() + self.session_manager = self.context.session_manager + self.user_id = "diana" + + def do_query(self, service_type, endpoint_type, request_args, state): + _client_service = self.client.get_service(service_type) + req_info = _client_service.get_request_parameters(request_args=request_args, state=state) + + areq = req_info.get("request") + headers = req_info.get("headers") + + _server_endpoint = self.server.get_endpoint(endpoint_type) + if areq: + if headers: + argv = {"http_info": {"headers": headers}} + else: + argv = {} + areq.lax = True + _req = areq.serialize(_server_endpoint.request_format) + _pr_resp = _server_endpoint.parse_request(_req, **argv) + else: + _pr_resp = _server_endpoint.parse_request(areq) + + if is_error_message(_pr_resp): + return areq, _pr_resp + + _resp = _server_endpoint.process_request(_pr_resp) + if is_error_message(_resp): + return areq, _resp + + _response = _server_endpoint.do_response(**_resp) + + resp = _client_service.parse_response(_response["response"]) + _client_service.update_service_context(_resp["response_args"], key=state) + return areq, resp + + def process_setup(self, token=None, scope=None): + # ***** Discovery ********* + + _req, _resp = self.do_query("server_metadata", "server_metadata", {}, "") + + # ***** Pushed Authorization Request ********** + _nonce = (rndstr(24),) + _context = self.client.get_service_context() + # Need a new state for a new authorization request + _state = _context.cstate.create_state(iss=_context.get("issuer")) + _context.cstate.bind_key(_nonce, _state) + + req_args = {"response_type": ["code"], "nonce": _nonce, "state": _state} + + if scope: + _scope = scope + else: + _scope = ["openid"] + + if token and list(token.keys())[0] == "refresh_token": + _scope = ["openid", "offline_access"] + + req_args["scope"] = _scope + + areq, auth_response = self.do_query("pushed_authorization", + "pushed_authorization", + req_args, + _state) + + # ***** Authorization Request ********** + _context = self.client.get_service_context() + + req_args = {"request_uri": auth_response["request_uri"], "response_type": ["code"]} + + areq, auth_response = self.do_query("authorization", "authorization", req_args, _state) + + # ***** Token Request ********** + + req_args = { + "code": auth_response["code"], + "state": auth_response["state"], + "redirect_uri": areq["redirect_uri"], + "grant_type": "authorization_code", + "client_id": self.client.get_client_id(), + "client_secret": _context.get_usage("client_secret"), + } + + _token_request, resp = self.do_query("accesstoken", "token", req_args, _state) + + return resp, _state, _scope + + def test_flow(self): + """ + Test that token exchange requests work correctly + """ + + resp, _state, _scope = self.process_setup(token="access_token", scope=["foobar"]) + + # Construct the resource request + + _client_service = self.client.get_service("resource") + req_info = _client_service.get_request_parameters( + authn_method="bearer_header", state=_state, endpoint="https://resource.example.com" + ) + + assert req_info["url"] == "https://resource.example.com" + assert "Authorization" in req_info["headers"] + assert req_info["headers"]["Authorization"].startswith("Bearer") From 3d9e932b9333e5c8980ceb95f9a003ff7b34b94d Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 28 Nov 2024 08:59:30 +0100 Subject: [PATCH 14/21] Minor editorial changes Fixed tests. Made sure there was a reasonable value on _service in authorization.py Authorization.oidc_post_construct() When creating request_args use local info. --- src/idpyoidc/client/oidc/access_token.py | 5 +++-- src/idpyoidc/client/oidc/authorization.py | 4 ++++ src/idpyoidc/client/oidc/registration.py | 6 ++++-- tests/private/token_jwks.json | 2 +- tests/test_client_21_oidc_service.py | 22 ---------------------- tests/test_client_27_conversation.py | 3 --- 6 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/idpyoidc/client/oidc/access_token.py b/src/idpyoidc/client/oidc/access_token.py index 2024612c..91736d5e 100644 --- a/src/idpyoidc/client/oidc/access_token.py +++ b/src/idpyoidc/client/oidc/access_token.py @@ -2,6 +2,7 @@ from typing import Optional from typing import Union +from idpyoidc.alg_info import get_signing_algs from idpyoidc.client.client_auth import get_client_authn_methods from idpyoidc.client.exception import ParameterError from idpyoidc.client.oauth2 import access_token @@ -9,7 +10,6 @@ from idpyoidc.message import Message from idpyoidc.message import oidc from idpyoidc.message.oidc import verified_claim_name -from idpyoidc.alg_info import get_signing_algs from idpyoidc.time_util import time_sans_frac __author__ = "Roland Hedberg" @@ -34,7 +34,8 @@ def __init__(self, upstream_get, conf: Optional[dict] = None): access_token.AccessToken.__init__(self, upstream_get, conf=conf) def gather_verify_arguments( - self, response: Optional[Union[dict, Message]] = None, behaviour_args: Optional[dict] = None + self, response: Optional[Union[dict, Message]] = None, + behaviour_args: Optional[dict] = None ): """ Need to add some information before running verify() diff --git a/src/idpyoidc/client/oidc/authorization.py b/src/idpyoidc/client/oidc/authorization.py index d1393726..9eb8c658 100644 --- a/src/idpyoidc/client/oidc/authorization.py +++ b/src/idpyoidc/client/oidc/authorization.py @@ -224,6 +224,10 @@ def oidc_post_construct(self, req, **kwargs): _req = None # just a flag kwargs["req"] = req + _service = kwargs.get("service", None) + if _service is None: + kwargs["service"] = self + if _request_param == "request_uri": kwargs["base_path"] = _context.get("base_url") + "/" + "requests" kwargs["local_dir"] = _context.get_usage("requests_dir", "./requests") diff --git a/src/idpyoidc/client/oidc/registration.py b/src/idpyoidc/client/oidc/registration.py index 49339053..435c7f7c 100644 --- a/src/idpyoidc/client/oidc/registration.py +++ b/src/idpyoidc/client/oidc/registration.py @@ -4,6 +4,7 @@ from idpyoidc.client.entity import response_types_to_grant_types from idpyoidc.client.service import Service +from idpyoidc.key_import import import_jwks from idpyoidc.message import oidc from idpyoidc.message.oauth2 import ResponseMessage @@ -75,7 +76,7 @@ def update_service_context(self, resp, key="", **kwargs): _keyjar = self.upstream_get("attribute", "keyjar") if _keyjar: if _client_id not in _keyjar: - _keyjar.import_jwks(_keyjar.export_jwks(True, ""), issuer_id=_client_id) + _keyjar = import_jwks(_keyjar, _keyjar.export_jwks(True, ""), _client_id) _client_secret = _context.get_usage("client_secret") if _client_secret: if not _keyjar: @@ -102,7 +103,8 @@ def gather_request_args(self, **kwargs): @return: """ _context = self.upstream_get("context") - req_args = _context.claims.create_registration_request() + req_args = _context.claims.get_client_metadata(metadata_schema=self.msg_type, + supported=_context.supports()) if "request_args" in self.conf: req_args.update(self.conf["request_args"]) diff --git a/tests/private/token_jwks.json b/tests/private/token_jwks.json index d3e0f070..9a4ae9d6 100644 --- a/tests/private/token_jwks.json +++ b/tests/private/token_jwks.json @@ -1 +1 @@ -{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "vrjoMrmgK8SmJJPc318zTxqG_tvBqF5l"}]} \ No newline at end of file +{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "ZcwEWWiviH92lCBx0NCAtZIHbK22je6S"}]} \ No newline at end of file diff --git a/tests/test_client_21_oidc_service.py b/tests/test_client_21_oidc_service.py index f0d83006..6f242dff 100644 --- a/tests/test_client_21_oidc_service.py +++ b/tests/test_client_21_oidc_service.py @@ -905,16 +905,12 @@ def test_construct(self): assert isinstance(_req, RegistrationRequest) assert set(_req.keys()) == { "application_type", - 'callback_uris', "default_max_age", - 'encrypt_request_object_supported', - 'encrypt_userinfo_supported', "grant_types", "id_token_signed_response_alg", "jwks", "redirect_uris", "request_object_signing_alg", - 'requests_dir', "response_modes", "response_types", "subject_type", @@ -932,17 +928,13 @@ def test_config_with_post_logout(self): assert isinstance(_req, RegistrationRequest) assert set(_req.keys()) == { "application_type", - 'callback_uris', "default_max_age", - 'encrypt_request_object_supported', - 'encrypt_userinfo_supported', "grant_types", "id_token_signed_response_alg", "jwks", "post_logout_redirect_uri", "redirect_uris", "request_object_signing_alg", - 'requests_dir', "response_modes", "response_types", "subject_type", @@ -979,20 +971,13 @@ def test_config_with_required_request_uri(): _req = reg_service.construct() assert isinstance(_req, RegistrationRequest) assert set(_req.keys()) == {'application_type', - 'callback_uris', - 'client_id', - 'client_secret', 'default_max_age', - 'encrypt_request_object_supported', - 'encrypt_userinfo_supported', 'grant_types', 'id_token_signed_response_alg', 'jwks', 'redirect_uris', 'request_object_signing_alg', - 'request_parameter', 'request_uris', - 'requests_dir', 'response_modes', 'response_types', 'subject_type', @@ -1039,20 +1024,13 @@ def test_config_logout_uri(): _req = reg_service.construct() assert isinstance(_req, RegistrationRequest) assert set(_req.keys()) == {'application_type', - 'callback_uris', - 'client_id', - 'client_secret', 'default_max_age', - 'encrypt_request_object_supported', - 'encrypt_userinfo_supported', 'grant_types', 'id_token_signed_response_alg', 'jwks', 'redirect_uris', 'request_object_signing_alg', - 'request_parameter', 'request_uris', - 'requests_dir', 'response_modes', 'response_types', 'subject_type', diff --git a/tests/test_client_27_conversation.py b/tests/test_client_27_conversation.py index 99264876..effe9553 100644 --- a/tests/test_client_27_conversation.py +++ b/tests/test_client_27_conversation.py @@ -401,11 +401,8 @@ def test_conversation(): "application_type", "backchannel_logout_session_required", "backchannel_logout_uri", - 'callback_uris', "contacts", "default_max_age", - 'encrypt_request_object_supported', - 'encrypt_userinfo_supported', "grant_types", "id_token_signed_response_alg", "jwks", From b01795ea9db07c9486510aecefa2006c522b7127 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 28 Nov 2024 09:51:58 +0100 Subject: [PATCH 15/21] Big change is to use urlsafe base64 encoded digest of the hash instead of the hexdigest. --- src/idpyoidc/server/oauth2/add_on/dpop.py | 74 ++++++++++++++++------- tests/test_server_60_dpop.py | 5 +- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/src/idpyoidc/server/oauth2/add_on/dpop.py b/src/idpyoidc/server/oauth2/add_on/dpop.py index 2e7ae1e5..24209629 100644 --- a/src/idpyoidc/server/oauth2/add_on/dpop.py +++ b/src/idpyoidc/server/oauth2/add_on/dpop.py @@ -1,3 +1,4 @@ +import base64 import logging from hashlib import sha256 from typing import Callable @@ -8,13 +9,14 @@ from cryptojwt import JWS from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jws.jws import factory +from cryptojwt.utils import add_padding +from idpyoidc.alg_info import get_signing_algs from idpyoidc.message import Message from idpyoidc.message import SINGLE_OPTIONAL_STRING from idpyoidc.message import SINGLE_REQUIRED_INT from idpyoidc.message import SINGLE_REQUIRED_JSON from idpyoidc.message import SINGLE_REQUIRED_STRING -from idpyoidc.alg_info import get_signing_algs from idpyoidc.server.client_authn import BearerHeader logger = logging.getLogger(__name__) @@ -107,7 +109,14 @@ def token_post_parse_request(request, client_id, context, **kwargs): if not _http_info: return request - _dpop = DPoPProof().verify_header(_http_info["headers"]["dpop"]) + _headers = _http_info['headers'] + logger.debug(f"http headers: {_headers}") + + _dpop_header = _headers.get("dpop", _headers.get("http_dpop", None)) + if not _dpop_header: + raise ValueError("Missing DPoP header") + + _dpop = DPoPProof().verify_header(_dpop_header) # The signature of the JWS is verified, now for checking the # content @@ -130,7 +139,7 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs """ Expect http_info attribute in kwargs. http_info should be a dictionary containing HTTP information. - This function is ment for DPoP-protected resources. + This function is meant for DPoP-protected resources. :param request: :param client_id: @@ -144,6 +153,18 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs return request _dpop = DPoPProof().verify_header(_http_info["headers"]["dpop"]) + _headers = _http_info.get("headers", "") + if _headers: + _dpop_header = _headers.get("dpop", "") + if not _dpop_header: + _dpop_header = _headers.get("http_dpop", "") + if not _dpop_header: + logger.debug(f"Request Headers: {_headers}") + raise ValueError("Expected DPoP header, none found") + else: + raise ValueError("Expected DPoP header, no headers found") + + _dpop = DPoPProof().verify_header(_dpop_header) # The signature of the JWS is verified, now for checking the # content @@ -158,9 +179,19 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs _dpop.key = key_from_jwk_dict(_dpop["jwk"]) ath = sha256(auth_info["token"].encode("utf8")).hexdigest() + _token = auth_info.get("token", None) + if _token: + ath = as_unicode(base64.urlsafe_b64encode(sha256(_token.encode("utf8")).digest())) if _dpop["ath"] != ath: - raise ValueError("'ath' in DPoP does not match the token hash") + _ath = _dpop.get("ath", None) + if _ath is None: + raise ValueError("'ath' missing from DPoP") + else: + _athb = _ath.rstrip("=") + _ath = add_padding(_athb) + if _ath != ath: + raise ValueError("'ath' in DPoP does not match the token hash") # Need something I can add as a reference when minting tokens request["dpop_jkt"] = as_unicode(_dpop.key.thumbprint("SHA-256")) @@ -184,34 +215,32 @@ def _add_to_context(endpoint, algs_supported): _context = endpoint.upstream_get("context") _context.provider_info["dpop_signing_alg_values_supported"] = algs_supported _context.add_on["dpop"] = {"algs_supported": algs_supported} - _context.client_authn_methods["dpop"] = DPoPClientAuth - + _context.client_authn_methods["dpop"] = DPoPClientAuth(endpoint.upstream_get) -def add_support(endpoint: dict, **kwargs): - # Pick the token endpoint - _endp = endpoint.get("token", None) - if _endp: - _endp.post_parse_request.append(token_post_parse_request) - _added_to_context = False - _algs_supported = kwargs.get("dpop_signing_alg_values_supported") - if not _algs_supported: +def add_support(endpoint: dict, dpop_signing_alg_values_supported=None, dpop_endpoints=None, **kwargs): + if dpop_signing_alg_values_supported is None: _algs_supported = ["RS256"] else: - _algs_supported = [alg for alg in _algs_supported if alg in get_signing_algs()] + # Pick out the ones I support + _algs_supported = [alg for alg in dpop_signing_alg_values_supported if alg in get_signing_algs()] + + _added_to_context = False - if _endp: - _add_to_context(_endp, _algs_supported) - _added_to_context = True + if dpop_endpoints is None: + dpop_endpoints = ["userinfo"] - for _dpop_endpoint in kwargs.get("dpop_endpoints", ["userinfo"]): + for _dpop_endpoint in dpop_endpoints: _endpoint = endpoint.get(_dpop_endpoint, None) if _endpoint: if not _added_to_context: - _add_to_context(_endp, _algs_supported) + _add_to_context(_endpoint, _algs_supported) _added_to_context = True - _endpoint.post_parse_request.append(userinfo_post_parse_request) + if _endpoint.name == "userinfo": + _endpoint.post_parse_request.append(userinfo_post_parse_request) + elif _endpoint.name == "token": + _endpoint.post_parse_request.append(token_post_parse_request) # DPoP-bound access token in the "Authorization" header and the DPoP proof in the "DPoP" header @@ -220,7 +249,7 @@ def add_support(endpoint: dict, **kwargs): class DPoPClientAuth(BearerHeader): tag = "dpop_client_auth" - def is_usable(self, request=None, authorization_token=None, http_headers=None): + def is_usable(self, request=None, authorization_token=None, http_info=None): if authorization_token is not None and authorization_token.startswith("DPoP "): return True return False @@ -231,6 +260,7 @@ def verify( authorization_token: Optional[str] = None, endpoint=None, # Optional[Endpoint] get_client_id_from_token: Optional[Callable] = None, + http_info: Optional[dict] = None, **kwargs, ): # info contains token and client_id diff --git a/tests/test_server_60_dpop.py b/tests/test_server_60_dpop.py index e13b8a35..44a88588 100644 --- a/tests/test_server_60_dpop.py +++ b/tests/test_server_60_dpop.py @@ -115,7 +115,10 @@ def create_endpoint(self): "add_on": { "dpop": { "function": "idpyoidc.server.oauth2.add_on.dpop.add_support", - "kwargs": {"dpop_signing_alg_values_supported": ["ES256"]}, + "kwargs": { + "dpop_signing_alg_values_supported": ["ES256"], + "dpop_endpoints": ["token"] + }, }, }, "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, From 4141eebc4c134d84a1084fc11f061f394bb4a5f4 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 28 Nov 2024 10:55:30 +0100 Subject: [PATCH 16/21] Editorial. No encryption value defaults (provider_info) Get request information dynamically. {registration} Use keyjar method wrappers --- src/idpyoidc/server/oidc/authorization.py | 4 +- .../server/oidc/backchannel_authentication.py | 31 +++++++-------- src/idpyoidc/server/oidc/provider_config.py | 11 +++++- src/idpyoidc/server/oidc/registration.py | 38 +++++++++++++++---- .../server/oidc/token_helper/access_token.py | 5 ++- src/idpyoidc/server/oidc/userinfo.py | 4 +- 6 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/idpyoidc/server/oidc/authorization.py b/src/idpyoidc/server/oidc/authorization.py index 29e93886..055774f3 100644 --- a/src/idpyoidc/server/oidc/authorization.py +++ b/src/idpyoidc/server/oidc/authorization.py @@ -83,8 +83,8 @@ class Authorization(authorization.Authorization): "claims_parameter_supported": True, "encrypt_request_object_supported": False, "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), - "request_object_encryption_alg_values_supported": alg_info.get_encryption_algs(), - "request_object_encryption_enc_values_supported": alg_info.get_encryption_encs(), + "request_object_encryption_alg_values_supported": [], + "request_object_encryption_enc_values_supported": [], "request_parameter_supported": None, "request_uri_parameter_supported": None, "require_request_uri_registration": None, diff --git a/src/idpyoidc/server/oidc/backchannel_authentication.py b/src/idpyoidc/server/oidc/backchannel_authentication.py index b193e223..456fb251 100644 --- a/src/idpyoidc/server/oidc/backchannel_authentication.py +++ b/src/idpyoidc/server/oidc/backchannel_authentication.py @@ -86,10 +86,10 @@ def allowed_target_uris(self): return set(res) def process_request( - self, - request: Optional[Union[Message, dict]] = None, - http_info: Optional[dict] = None, - **kwargs, + self, + request: Optional[Union[Message, dict]] = None, + http_info: Optional[dict] = None, + **kwargs, ): try: request_user = self.do_request_user(request) @@ -125,6 +125,7 @@ def process_request( class CIBATokenHelper(AccessTokenHelper): + def _get_session_info(self, request, session_manager): _path = request["_session_path"] _grant = session_manager.get(_path) @@ -137,7 +138,7 @@ def _get_session_info(self, request, session_manager): return session_info, _grant def post_parse_request( - self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ) -> Union[Message, dict]: _context = self.endpoint.upstream_get("context") _mngr = _context.session_manager @@ -303,10 +304,10 @@ def __init__(self, upstream_get: Callable, **kwargs): Endpoint.__init__(self, upstream_get, **kwargs) def process_request( - self, - request: Optional[Union[Message, dict]] = None, - http_info: Optional[dict] = None, - **kwargs, + self, + request: Optional[Union[Message, dict]] = None, + http_info: Optional[dict] = None, + **kwargs, ) -> Union[Message, dict]: return {} @@ -316,17 +317,17 @@ class ClientNotificationAuthn(ClientSecretBasic): tag = "client_notification_authn" - def is_usable(self, request=None, authorization_token=None): + def is_usable(self, request=None, authorization_token=None, http_info=None): if authorization_token is not None and authorization_token.startswith("Bearer "): return True return False def _verify( - self, - authorization_token: Optional[str] = None, - endpoint=None, # Optional[Endpoint] - get_client_id_from_token: Optional[Callable] = None, - **kwargs, + self, + authorization_token: Optional[str] = None, + endpoint=None, # Optional[Endpoint] + get_client_id_from_token: Optional[Callable] = None, + **kwargs, ): ttype, token = authorization_token.split(" ", 1) if ttype != "Bearer": diff --git a/src/idpyoidc/server/oidc/provider_config.py b/src/idpyoidc/server/oidc/provider_config.py index 819a6997..be399ae6 100755 --- a/src/idpyoidc/server/oidc/provider_config.py +++ b/src/idpyoidc/server/oidc/provider_config.py @@ -33,4 +33,13 @@ def add_endpoints(self, request, client_id, context, **kwargs): return request def process_request(self, request=None, **kwargs): - return {"response_args": self.upstream_get("context").provider_info} + _schema = self.upstream_get("attribute", "metadata_schema") + _args = self.upstream_get("context").claims.get_server_metadata(metadata_schema=_schema) + # add issuer + _args["issuer"] = self.upstream_get("attribute", "entity_id") + # add endpoints + for name, endpoint in self.upstream_get("unit").endpoint.items(): + if endpoint.endpoint_name: + _args[endpoint.endpoint_name] = endpoint.full_path + + return {"response_args": _args} diff --git a/src/idpyoidc/server/oidc/registration.py b/src/idpyoidc/server/oidc/registration.py index a363ebeb..b775b406 100644 --- a/src/idpyoidc/server/oidc/registration.py +++ b/src/idpyoidc/server/oidc/registration.py @@ -10,6 +10,9 @@ from cryptojwt.jws.utils import alg2keytype from cryptojwt.utils import as_bytes +from idpyoidc.key_import import import_jwks + +from idpyoidc.key_import import import_jwks_as_json from idpyoidc.exception import MessageException from idpyoidc.message.oauth2 import ResponseMessage @@ -143,7 +146,7 @@ def match_claim(self, claim, val): # Use my defaults _my_key = _context.claims.register2preferred.get(claim, claim) try: - _val = _context.provider_info[_my_key] + _val = _context.claims.get_preference(_my_key) except KeyError: return val @@ -279,9 +282,18 @@ def do_client_registration(self, request, client_id, ignore=None): t = {"jwks_uri": "", "jwks": None} - for item in ["jwks_uri", "jwks"]: - if item in request: - t[item] = request[item] + _jwks_uri = request.get("jwks_uri") + if _jwks_uri: + # if it can't load keys because the URL is false it will + # just silently fail. Waiting for better times. + _keyjar.add_url(issuer_id=client_id, url=_jwks_uri) + else: + _jwks = request.get("jwks", None) + if _jwks: + if isinstance(_jwks, str): + _keyjar = import_jwks_as_json(_keyjar, _jwks, client_id) + else: + _keyjar = import_jwks(_keyjar, _jwks, client_id) # if it can't load keys because the URL is false it will # just silently fail. Waiting for better times. @@ -437,7 +449,13 @@ def client_registration_setup(self, request, if not reserved_client_id: reserved_client_id = _context.cdb.keys() client_id = cid_generator(reserved=reserved_client_id, **cid_gen_kwargs) - if "client_id" in request: + _entity_id = request.get("client_id", None) + if _entity_id: + # Already registered + _old_id = _context.client_known_as.get(request["client_id"], None) + if _old_id: + del _context.cdb[_old_id] + _context.client_known_as[_entity_id] = client_id del request["client_id"] else: client_id = request.get("client_id") @@ -456,7 +474,7 @@ def client_registration_setup(self, request, if set_secret: client_secret = self.add_client_secret(_cinfo, client_id, _context) - logger.debug("Stored client info in CDB under cid={}".format(client_id)) + logger.debug(f"Stored client info in CDB under cid={client_id}") _context.cdb[client_id] = _cinfo _cinfo = self.do_client_registration( @@ -469,6 +487,12 @@ def client_registration_setup(self, request, args = dict([(k, v) for k, v in _cinfo.items() if k in self.response_cls.c_param]) + # Don't echo keys back + try: + del args["jwks"] + except KeyError: + pass + comb_uri(args) response = self.response_cls(**args) @@ -495,7 +519,7 @@ def process_request(self, request=None, new_id=True, set_secret=True, **kwargs): reg_resp = self.client_registration_setup(request, new_id, set_secret, reserved_client_id) except Exception as err: - logger.error("client_registration_setup: %s", request) + logger.exception(f"client_registration_setup: {request}") return ResponseMessage( error="invalid_configuration_request", error_description="%s" % err ) diff --git a/src/idpyoidc/server/oidc/token_helper/access_token.py b/src/idpyoidc/server/oidc/token_helper/access_token.py index 2594748e..eefc4e24 100755 --- a/src/idpyoidc/server/oidc/token_helper/access_token.py +++ b/src/idpyoidc/server/oidc/token_helper/access_token.py @@ -17,6 +17,7 @@ class AccessTokenHelper(TokenEndpointHelper): + def _get_session_info(self, request, session_manager): if request["grant_type"] != "authorization_code": return self.error_cls(error="invalid_request", error_description="Unknown grant_type") @@ -56,7 +57,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): if "grant_types_supported" in _context.cdb[client_id]: grant_types_supported = _context.cdb[client_id].get("grant_types_supported") else: - grant_types_supported = _context.provider_info["grant_types_supported"] + grant_types_supported = _context.provider_info.get("grant_types", []) grant = _session_info["grant"] token_type = "Bearer" @@ -166,7 +167,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): return _response def post_parse_request( - self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ) -> Union[Message, dict]: """ This is where clients come to get their access tokens diff --git a/src/idpyoidc/server/oidc/userinfo.py b/src/idpyoidc/server/oidc/userinfo.py index 27557047..54a8b93f 100755 --- a/src/idpyoidc/server/oidc/userinfo.py +++ b/src/idpyoidc/server/oidc/userinfo.py @@ -34,8 +34,8 @@ class UserInfo(Endpoint): "claim_types_supported": ["normal", "aggregated", "distributed"], "encrypt_userinfo_supported": True, "userinfo_signing_alg_values_supported": alg_info.get_signing_algs(), - "userinfo_encryption_alg_values_supported": alg_info.get_encryption_algs(), - "userinfo_encryption_enc_values_supported": alg_info.get_encryption_encs(), + "userinfo_encryption_alg_values_supported": [], + "userinfo_encryption_enc_values_supported": [], } def __init__( From 6578956dedb52ab6b5d310a12edd8ff0fd7e3ffa Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 2 Dec 2024 09:59:41 +0100 Subject: [PATCH 17/21] Raw fernet key must be of type bytes. --- src/idpyoidc/encrypter.py | 11 +++++++++++ src/idpyoidc/server/endpoint.py | 1 + 2 files changed, 12 insertions(+) diff --git a/src/idpyoidc/encrypter.py b/src/idpyoidc/encrypter.py index f9a2052a..844618f3 100644 --- a/src/idpyoidc/encrypter.py +++ b/src/idpyoidc/encrypter.py @@ -2,6 +2,7 @@ from typing import Optional from cryptojwt.key_jar import init_key_jar +from cryptojwt.utils import as_bytes from idpyoidc.util import instantiate @@ -98,6 +99,16 @@ def init_encrypter(conf: Optional[dict] = None): if attr == "keys": continue _kwargs[attr] = val + + _key = _kwargs.get("key") + if _key: + if isinstance(_key, bytes): + pass + elif isinstance(_key, str): + _kwargs["key"] = as_bytes(_key) + else: + raise ValueError("Raw key most be of type bytes") + return { "encrypter": instantiate(_class, **_kwargs), "conf": {"class": _class, "kwargs": _kwargs}, diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py index 6e41a6f7..038d234e 100755 --- a/src/idpyoidc/server/endpoint.py +++ b/src/idpyoidc/server/endpoint.py @@ -372,6 +372,7 @@ def _get_content_type(self, **kwargs): else: content_type = "application/x-www-form-urlencoded" return content_type + def do_response( self, response_args: Optional[dict] = None, From c120a7abc9a607c48ad9203727a9ed6b5e6f5834 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 2 Dec 2024 10:22:58 +0100 Subject: [PATCH 18/21] Corrected spelling error --- src/idpyoidc/client/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idpyoidc/client/util.py b/src/idpyoidc/client/util.py index eb57e32e..ed738897 100755 --- a/src/idpyoidc/client/util.py +++ b/src/idpyoidc/client/util.py @@ -275,7 +275,7 @@ def get_deserialization_method(reqresp): deser_method = "jose" elif match_to_(URL_ENCODED, _ctype): deser_method = "urlencoded" - elif match_to_("text/plain", _ctype) or match_to_("test/html", _ctype): + elif match_to_("text/plain", _ctype) or match_to_("text/html", _ctype): deser_method = "" else: deser_method = "" # reasonable default ?? From 2e7b03c69b49c6c42469224752df6c3f4fed7f98 Mon Sep 17 00:00:00 2001 From: roland Date: Tue, 3 Dec 2024 10:56:08 +0100 Subject: [PATCH 19/21] Remove patch files --- patch/alg_info.patch | 1167 --------- patch/claims.patch | 1370 ---------- patch/metadata.patch | 63 - patch/oauth2.patch | 954 ------- patch/oidc.patch | 687 ----- patch/test_client_30.patch | 158 -- patch/tests.patch | 5084 ------------------------------------ patch/transform.patch | 60 - 8 files changed, 9543 deletions(-) delete mode 100644 patch/alg_info.patch delete mode 100644 patch/claims.patch delete mode 100644 patch/metadata.patch delete mode 100644 patch/oauth2.patch delete mode 100644 patch/oidc.patch delete mode 100644 patch/test_client_30.patch delete mode 100644 patch/tests.patch delete mode 100644 patch/transform.patch diff --git a/patch/alg_info.patch b/patch/alg_info.patch deleted file mode 100644 index f15b7a2f..00000000 --- a/patch/alg_info.patch +++ /dev/null @@ -1,1167 +0,0 @@ -diff --git a/src/idpyoidc/__init__.py b/src/idpyoidc/__init__.py -index 76d83a3..834b77a 100644 ---- a/src/idpyoidc/__init__.py -+++ b/src/idpyoidc/__init__.py -@@ -1,5 +1,5 @@ - __author__ = "Roland Hedberg" --__version__ = "4.3.0" -+__version__ = "5.0.0" - - VERIFIED_CLAIM_PREFIX = "__verified" - -@@ -10,7 +10,7 @@ def verified_claim_name(claim): - - def proper_path(path): - """ -- Clean up the path specification so it looks like something I could use. -+ Clean up the path specification such that it looks like something I could use. - "./" "/" - """ - if path.startswith("./"): - -diff --git a/src/idpyoidc/alg_info.py b/src/idpyoidc/alg_info.py -new file mode 100644 -index 0000000..f3a9641 ---- /dev/null -+++ b/src/idpyoidc/alg_info.py -@@ -0,0 +1,67 @@ -+from functools import cmp_to_key -+import logging -+ -+from cryptojwt.jwe import DEPRECATED -+from cryptojwt.jwe import SUPPORTED -+from cryptojwt.jws.jws import SIGNER_ALGS -+ -+logger = logging.getLogger(__name__) -+ -+SIGNING_ALGORITHM_SORT_ORDER = ["RS", "ES", "PS", "HS", "Ed"] -+ -+ -+def cmp(a, b): -+ return (a > b) - (a < b) -+ -+ -+def alg_cmp(a, b): -+ if a == "none": -+ return 1 -+ elif b == "none": -+ return -1 -+ -+ _pos1 = SIGNING_ALGORITHM_SORT_ORDER.index(a[0:2]) -+ _pos2 = SIGNING_ALGORITHM_SORT_ORDER.index(b[0:2]) -+ if _pos1 == _pos2: -+ return (a > b) - (a < b) -+ elif _pos1 > _pos2: -+ return 1 -+ else: -+ return -1 -+ -+ -+def get_signing_algs(): -+ # Assumes Cryptojwt -+ _algs = [name for name in list(SIGNER_ALGS.keys()) if name != "none" and name not in DEPRECATED["alg"]] -+ return sorted(_algs, key=cmp_to_key(alg_cmp)) -+ -+ -+def get_encryption_algs(): -+ return SUPPORTED["alg"] -+ -+ -+def get_encryption_encs(): -+ return SUPPORTED["enc"] -+ -+ -+def array_or_singleton(claim_spec, values): -+ if isinstance(claim_spec[0], list): -+ if isinstance(values, list): -+ return values -+ else: -+ return [values] -+ else: -+ if isinstance(values, list): -+ return values[0] -+ else: # singleton -+ return values -+ -+ -+def is_subset(a, b): -+ if isinstance(a, list): -+ if isinstance(b, list): -+ return set(b).issubset(set(a)) -+ elif isinstance(b, list): -+ return a in b -+ else: -+ return a == b - -diff --git a/src/idpyoidc/claims.py b/src/idpyoidc/claims.py -index e684624..afa6680 100644 ---- a/src/idpyoidc/claims.py -+++ b/src/idpyoidc/claims.py -@@ -1,4 +1,6 @@ -+import logging - from typing import Callable -+from typing import List - from typing import Optional - - from cryptojwt import KeyJar -@@ -7,9 +9,14 @@ from cryptojwt.utils import importer - - from idpyoidc.client.util import get_uri - from idpyoidc.impexp import ImpExp -+from idpyoidc.key_import import import_jwks -+from idpyoidc.key_import import store_under_other_id -+from idpyoidc.message import Message -+from idpyoidc.transform import preferred_to_registered - from idpyoidc.util import add_path - from idpyoidc.util import qualified_name - -+logger = logging.getLogger(__name__) - - def claims_dump(info, exclude_attributes): - return {qualified_name(info.__class__): info.dump(exclude_attributes=exclude_attributes)} -@@ -85,7 +92,17 @@ class Claims(ImpExp): - self.callback = callbacks - - def verify_rules(self, supports): -- return True -+ if self.get_preference("encrypt_userinfo_supported", False) is True: -+ self.set_preference("userinfo_encryption_alg_values_supported", []) -+ self.set_preference("userinfo_encryption_enc_values_supported", []) -+ -+ if self.get_preference("encrypt_request_object_supported", False) is True: -+ self.set_preference("request_object_encryption_alg_values_supported", []) -+ self.set_preference("request_object_encryption_enc_values_supported", []) -+ -+ if self.get_preference("encrypt_id_token_supported", False) is True: -+ self.set_preference("id_token_encryption_alg_values_supported", []) -+ self.set_preference("id_token_encryption_enc_values_supported", []) - - def locals(self, info): - pass -@@ -104,11 +121,11 @@ class Claims(ImpExp): - else: - _keyjar = KeyJar() - if "jwks" in conf: -- _keyjar.import_jwks(conf["jwks"], "") -+ _keyjar = import_jwks(_keyjar, conf["jwks"], "") - - if "" in _keyjar and entity_id: - # make sure I have the keys under my own name too (if I know it) -- _keyjar.import_jwks_as_json(_keyjar.export_jwks_as_json(True, ""), entity_id) -+ _keyjar = store_under_other_id(_keyjar, "", entity_id, True) - - _httpc_params = conf.get("httpc_params") - if _httpc_params: -@@ -122,7 +139,7 @@ class Claims(ImpExp): - - return keyjar, _uri_path - -- def get_base_url(self, configuration: dict, entity_id: Optional[str]=""): -+ def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): - raise NotImplementedError() - - def get_id(self, configuration: dict): -@@ -138,6 +155,7 @@ class Claims(ImpExp): - configuration: dict, - keyjar: Optional[KeyJar] = None, - entity_id: Optional[str] = ""): -+ logger.debug(f"configuration: {configuration}") - _jwks = _jwks_uri = None - _id = self.get_id(configuration) - keyjar, uri_path = self._keyjar(keyjar, configuration, entity_id=_id) -@@ -180,6 +198,10 @@ class Claims(ImpExp): - elif val: - self.set_preference(key, val) - -+ for attr, val in supports.items(): -+ if attr not in self.prefer and val is not None: -+ self.set_preference(attr, val) -+ - self.verify_rules(supports) - return keyjar - -@@ -195,15 +217,21 @@ class Claims(ImpExp): - def construct_uris(self, *args): - pass - -- def supports(self): -+ def _expand(self, dictionary): - res = {} -- for key, val in self._supports.items(): -+ for key, val in dictionary.items(): - if isinstance(val, Callable): - res[key] = val() - else: -- res[key] = val -+ if isinstance(val, dict): -+ res[key] = self._expand(val) -+ else: -+ res[key] = val - return res - -+ def supports(self): -+ return self._expand(self._supports) -+ - def supported(self, claim): - return claim in self._supports - -@@ -219,3 +247,77 @@ class Claims(ImpExp): - return default - else: - return _val -+ -+ def get_endpoint_claims(self, endpoints): -+ _info = {} -+ for endp in endpoints: -+ if endp.endpoint_name: -+ _info[endp.endpoint_name] = endp.full_path -+ for arg, claim in [("client_authn_method", "auth_methods"), -+ ("auth_signing_alg_values", "auth_signing_alg_values")]: -+ _val = getattr(endp, arg, None) -+ if _val: -+ # trust_mark_status_endpoint_auth_methods_supported -+ md_param = f"{endp.endpoint_name}_{claim}" -+ _info[md_param] = _val -+ return _info -+ -+ def get_server_metadata(self, -+ entity_type: Optional[str] = "", -+ endpoints: Optional[list] = None, -+ metadata_schema: Optional[Message] = None, -+ extra_claims: Optional[List[str]] = None, -+ **kwargs): -+ -+ metadata = self.prefer -+ # the claims that can appear in the metadata -+ if metadata_schema: -+ attr = list(metadata_schema.c_param.keys()) -+ else: -+ attr = [] -+ -+ if extra_claims: -+ attr.extend(extra_claims) -+ -+ if attr: -+ metadata = {k: v for k, v in metadata.items() if k in attr and v != []} -+ -+ # collect endpoints -+ if endpoints: -+ metadata.update(self.get_endpoint_claims(endpoints)) -+ -+ if entity_type: -+ return {entity_type: metadata} -+ else: -+ return metadata -+ -+ def get_client_metadata(self, -+ entity_type: Optional[str] = "", -+ metadata_schema: Optional[Message] = None, -+ extra_claims: Optional[List[str]] = None, -+ supported: Optional[dict] = None, -+ **kwargs): -+ -+ if supported is None: -+ supported = self.supports() -+ -+ if not self.use: -+ self.use = preferred_to_registered(self.prefer, supported=supported) -+ -+ metadata = self.use -+ # the claims that can appear in the metadata -+ if metadata_schema: -+ attr = list(metadata_schema.c_param.keys()) -+ else: -+ attr = [] -+ -+ if extra_claims: -+ attr.extend(extra_claims) -+ -+ if attr: -+ metadata = {k: v for k, v in metadata.items() if k in attr} -+ -+ if entity_type: -+ return {entity_type: metadata} -+ else: -+ return metadata - -diff --git a/src/idpyoidc/client/entity_metadata.py b/src/idpyoidc/client/entity_metadata.py -new file mode 100644 -index 0000000..6b41b55 ---- /dev/null -+++ b/src/idpyoidc/client/entity_metadata.py -@@ -0,0 +1,36 @@ -+from typing import Optional -+ -+from idpyoidc.impexp import ImpExp -+ -+ -+class EntityMetadata(ImpExp): -+ parameter = {"metadata": {}} -+ def __init__(self, metadata: Optional[dict] = None): -+ ImpExp.__init__(self) -+ if metadata is None: -+ self.metadata = {} -+ else: -+ self.metadata = metadata -+ -+ def __getitem__(self, item): -+ for _type, _dict in self.metadata.items(): -+ _val = _dict.get(item, None) -+ if _val: -+ return _val -+ raise KeyError(item) -+ -+ def __setitem__(self, key, value): -+ # Assumes not multiple entity types -+ self.metadata[key] = value -+ -+ def items(self): -+ return self.metadata.items() -+ -+ def __contains__(self, item): -+ return item in self.metadata -+ -+ def get(self, item, default=None): -+ return self.metadata.get(item, default) -+ -+ def to_dict(self): -+ return self.metadata - - -diff --git a/src/idpyoidc/client/oauth2/access_token.py b/src/idpyoidc/client/oauth2/access_token.py -index 6ccb6f4..5161aa0 100644 ---- a/src/idpyoidc/client/oauth2/access_token.py -+++ b/src/idpyoidc/client/oauth2/access_token.py -@@ -7,7 +7,7 @@ from idpyoidc.client.oauth2.utils import get_state_parameter - from idpyoidc.client.service import Service - from idpyoidc.message import oauth2 - from idpyoidc.message.oauth2 import ResponseMessage --from idpyoidc.metadata import get_signing_algs -+from idpyoidc.alg_info import get_signing_algs - from idpyoidc.time_util import time_sans_frac - - LOGGER = logging.getLogger(__name__) - -diff --git a/src/idpyoidc/client/provider/github.py b/src/idpyoidc/client/provider/github.py -index 56c9103..674a78d 100644 ---- a/src/idpyoidc/client/provider/github.py -+++ b/src/idpyoidc/client/provider/github.py -@@ -1,12 +1,12 @@ -+from idpyoidc.alg_info import get_signing_algs - from idpyoidc.client.client_auth import get_client_authn_methods - from idpyoidc.client.oauth2 import access_token - from idpyoidc.client.oidc import userinfo -+from idpyoidc.message import Message - from idpyoidc.message import SINGLE_OPTIONAL_STRING - from idpyoidc.message import SINGLE_REQUIRED_STRING --from idpyoidc.message import Message - from idpyoidc.message import oauth2 - from idpyoidc.message.oauth2 import ResponseMessage --from idpyoidc.metadata import get_signing_algs - - - class AccessTokenResponse(Message): -diff --git a/src/idpyoidc/client/provider/linkedin.py b/src/idpyoidc/client/provider/linkedin.py -index 17c7e85..e0bc430 100644 ---- a/src/idpyoidc/client/provider/linkedin.py -+++ b/src/idpyoidc/client/provider/linkedin.py -@@ -1,13 +1,13 @@ -+from idpyoidc.alg_info import get_signing_algs - from idpyoidc.client.client_auth import get_client_authn_methods - from idpyoidc.client.oauth2 import access_token - from idpyoidc.client.oidc import userinfo -+from idpyoidc.message import Message - from idpyoidc.message import SINGLE_OPTIONAL_JSON - from idpyoidc.message import SINGLE_OPTIONAL_STRING - from idpyoidc.message import SINGLE_REQUIRED_INT - from idpyoidc.message import SINGLE_REQUIRED_STRING --from idpyoidc.message import Message - from idpyoidc.message import oauth2 --from idpyoidc.metadata import get_signing_algs - - - class AccessTokenResponse(Message): - - -diff --git a/src/idpyoidc/client/rp_handler.py b/src/idpyoidc/client/rp_handler.py -index eea94c0..33d02f5 100644 ---- a/src/idpyoidc/client/rp_handler.py -+++ b/src/idpyoidc/client/rp_handler.py -@@ -3,19 +3,24 @@ import sys - import traceback - from typing import List - from typing import Optional -+from typing import Union - - from cryptojwt import KeyJar -+from cryptojwt.key_jar import build_keyjar - from cryptojwt.key_jar import init_key_jar - from cryptojwt.utils import as_bytes - from cryptojwt.utils import importer - -+from idpyoidc.client.configure import RPHConfiguration - from idpyoidc.client.defaults import DEFAULT_CLIENT_CONFIGS - from idpyoidc.client.defaults import DEFAULT_OIDC_SERVICES --from idpyoidc.client.defaults import DEFAULT_RP_KEY_DEFS - from idpyoidc.client.oauth2.stand_alone_client import StandAloneClient -+from idpyoidc.configure import Base - from idpyoidc.util import add_path - from idpyoidc.util import rndstr -+from .defaults import DEFAULT_KEY_DEFS - from .oauth2 import Client -+from ..key_import import import_jwks - from ..message import Message - - logger = logging.getLogger(__name__) -@@ -34,50 +39,51 @@ class RPHandler(object): - state_db=None, - httpc=None, - httpc_params=None, -- config=None, -+ config: Optional[Union[dict, Base]] = None, - **kwargs, - ): -- self.base_url = base_url -- -- if keyjar is None: -- keyjar_defs = {} -- if config: -- keyjar_defs = getattr(config, "key_conf", None) -- -- if not keyjar_defs: -- keyjar_defs = kwargs.get("key_conf", DEFAULT_RP_KEY_DEFS) -- -- _jwks_path = kwargs.get("jwks_path", keyjar_defs.get("uri_path", keyjar_defs.get("public_path", ""))) -- if "uri_path" in keyjar_defs: -- del keyjar_defs["uri_path"] -- self.keyjar = init_key_jar(**keyjar_defs, issuer_id="") -- self.keyjar.import_jwks_as_json(self.keyjar.export_jwks_as_json(True, ""), base_url) -- else: -+ if config is None: -+ config = RPHConfiguration({}) -+ elif isinstance(config, dict): -+ config = RPHConfiguration(config) -+ -+ self.base_url = base_url or config.get("base_url", config.get("entity_id", "")) -+ self.entity_id = config.get("entity_id", config.conf.get("entity_id", self.base_url)) -+ self.entity_type = config.get("entity_type", config.conf.get("entity_type", "")) -+ self.client_type = config.get("client_type", config.conf.get("client_type", "")) -+ self.client_configs = client_configs or {} -+ -+ if keyjar: - self.keyjar = keyjar - _jwks_path = kwargs.get("jwks_path", "") -- -- if _jwks_path: -- self.jwks_uri = add_path(base_url, _jwks_path) -- else: -- self.jwks_uri = "" -- if len(self.keyjar): -- self.jwks = self.keyjar.export_jwks() -+ if _jwks_path: -+ self.jwks_uri = add_path(base_url, _jwks_path) - else: -- self.jwks = {} -+ self.jwks_uri = "" -+ if len(self.keyjar): -+ self.jwks = self.keyjar.export_jwks() -+ else: -+ self.jwks = {} - - if config: - if not hash_seed: - self.hash_seed = config.hash_seed -- if not keyjar: -- self.keyjar = init_key_jar(**config.key_conf, issuer_id="") -- if not client_configs: -- self.client_configs = config.clients -- -- if "client_class" in config: -- if isinstance(config["client_class"], str): -- self.client_cls = importer(config["client_class"]) -+ -+ if not keyjar and config.key_conf: -+ _conf = {k: v for k, v in config.key_conf.items() if k != "uri_path"} -+ self.keyjar = init_key_jar(**_conf, issuer_id="") -+ _jwks_path = kwargs.get("jwks_path", -+ config.key_conf.get("uri_path", -+ config.key_conf.get("public_path", ""))) -+ if _jwks_path: -+ self.jwks_uri = add_path(self.base_url, _jwks_path) -+ -+ _c_class = config.get("client_class", config.conf.get("client_class")) -+ if _c_class: -+ if isinstance(_c_class, str): -+ self.client_cls = importer(_c_class) - else: # assume it's a class -- self.client_cls = config["client_class"] -+ self.client_cls = _c_class - else: - self.client_cls = StandAloneClient - else: -@@ -86,23 +92,22 @@ class RPHandler(object): - else: - self.hash_seed = as_bytes(rndstr(32)) - -- if client_configs is None: -- self.client_configs = DEFAULT_CLIENT_CONFIGS -- for param in ["client_type", "preference", "add_ons"]: -- val = kwargs.get(param, None) -- if val: -- self.client_configs[""][param] = val -- else: -- self.client_configs = client_configs -- - _cc = kwargs.get("client_class", None) - if _cc: - if isinstance(_cc, str): - _cc = importer(_cc) -- self.client_cls =_cc -+ self.client_cls = _cc - else: - self.client_cls = StandAloneClient - -+ if client_configs is None: -+ self.client_configs = DEFAULT_CLIENT_CONFIGS -+ for param in ["client_type", "preference", "add_ons"]: -+ val = kwargs.get(param, None) -+ if val: -+ self.client_configs[""][param] = val -+ else: -+ self.client_configs = client_configs - - if state_db: - self.state_db = state_db -@@ -111,6 +116,9 @@ class RPHandler(object): - - self.extra = kwargs - -+ if services is None: -+ services = config.get("services", config.conf.get("services", None)) -+ - if services is None: - self.services = DEFAULT_OIDC_SERVICES - else: -@@ -122,15 +130,20 @@ class RPHandler(object): - self.httpc = httpc - - if not httpc_params: -- self.httpc_params = {"verify": verify_ssl} -+ self.httpc_params = config.get("httpc_params", {"verify": verify_ssl}) - else: - self.httpc_params = httpc_params - -- if not self.keyjar.httpc_params: -- self.keyjar.httpc_params = self.httpc_params -- - self.upstream_get = kwargs.get("upstream_get", None) - -+ _keyjar = getattr(self, "keyjar", None) -+ if _keyjar is not None: -+ if not _keyjar.httpc_params: -+ _keyjar.httpc_params = getattr(self, "httpc_params", {}) -+ else: -+ self.keyjar = build_keyjar(DEFAULT_KEY_DEFS) -+ self.keyjar.httpc_params = getattr(self, "httpc_params", {}) -+ - def state2issuer(self, state): - """ - Given the state value find the Issuer ID of the OP/AS that state value -@@ -159,7 +172,13 @@ class RPHandler(object): - :param issuer: Issuer ID - :return: A client configuration - """ -- return self.client_configs[issuer] -+ _cnf = self.client_configs[issuer].copy() -+ for param in ["entity_id", "client_id", "base_url", "services", "jwks_uri", "entity_type", -+ "client_type"]: -+ if param not in _cnf and getattr(self, param, None): -+ _cnf[param] = getattr(self, param) -+ -+ return _cnf - - def get_session_information(self, key, client=None): - """ -@@ -192,16 +211,7 @@ class RPHandler(object): - _cnf = self.pick_config("") - _cnf["issuer"] = issuer - -- try: -- _services = _cnf["services"] -- except KeyError: -- _services = self.services -- -- if "base_url" not in _cnf: -- _cnf["base_url"] = self.base_url -- -- if self.jwks_uri: -- _cnf["jwks_uri"] = self.jwks_uri -+ _services = _cnf["services"] - - logger.debug(f"config: {_cnf}") - try: -@@ -221,20 +231,29 @@ class RPHandler(object): - _context = client.get_context() - if _context.iss_hash: - self.hash2issuer[_context.iss_hash] = issuer -+ - # If non persistent - _keyjar = client.keyjar -- if not _keyjar: -+ if _keyjar is None: - _keyjar = KeyJar() - _keyjar.httpc_params.update(self.httpc_params) - -- for iss in self.keyjar.owners(): -- _keyjar.import_jwks(self.keyjar.export_jwks(issuer_id=iss, private=True), iss) -+ if self.upstream_get: -+ _srv_keyjar = self.upstream_get("attribute", "keyjar") -+ else: -+ _srv_keyjar = getattr(self, "keyjar", None) -+ -+ if _srv_keyjar: -+ for iss in _srv_keyjar.owners(): -+ _keyjar = import_jwks(_keyjar, self.keyjar.export_jwks(issuer_id=iss, private=True), iss) - - client.keyjar = _keyjar - # If persistent nothing has to be copied - -- _context.base_url = self.base_url -- _context.jwks_uri = self.jwks_uri -+ for item in ["jwks_uri", "base_url"]: -+ _val = getattr(self, item, None) -+ if _val: -+ setattr(_context, item, _val) - return client - - def do_provider_info( -@@ -639,7 +658,8 @@ class RPHandler(object): - return client.logout(state, post_logout_redirect_uri=post_logout_redirect_uri) - - def close( -- self, state: str, issuer: Optional[str] = "", post_logout_redirect_uri: Optional[str] = "" -+ self, state: str, issuer: Optional[str] = "", -+ post_logout_redirect_uri: Optional[str] = "" - ) -> dict: - - if issuer: - -diff --git a/src/idpyoidc/key_import.py b/src/idpyoidc/key_import.py -new file mode 100644 -index 0000000..9b33f50 ---- /dev/null -+++ b/src/idpyoidc/key_import.py -@@ -0,0 +1,76 @@ -+import json -+from typing import List -+from typing import Optional -+ -+from cryptojwt import JWK -+from cryptojwt import KeyBundle -+from cryptojwt import KeyJar -+from cryptojwt.jwk.hmac import SYMKey -+from cryptojwt.jwk.jwk import key_from_jwk_dict -+ -+ -+def issuer_keys(keyjar: KeyJar, entity_id: str, format: Optional[str] = "jwk"): -+ # sort of copying the functionality in KeyJar.get_issuer_keys() -+ key_issuer = keyjar.return_issuer(entity_id) -+ if format == "jwk": -+ return [k.serialize() for k in key_issuer.all_keys()] -+ else: -+ return [k for k in key_issuer.all_keys()] -+ -+ -+def import_jwks(keyjar: KeyJar, jwks: dict, entity_id: Optional[str] = "") -> KeyJar: -+ keys = [] -+ jar = issuer_keys(keyjar, entity_id) -+ for jwk in jwks["keys"]: -+ if jwk not in jar: -+ jar.append(jwk) -+ key = key_from_jwk_dict(jwk) -+ keys.append(key) -+ if keys: -+ keyjar.add_keys(entity_id, keys) -+ return keyjar -+ -+ -+def import_jwks_as_json(keyjar: KeyJar, jwks: str, entity_id: Optional[str] = "") -> KeyJar: -+ return import_jwks(keyjar, json.loads(jwks), entity_id) -+ -+ -+def import_jwks_from_file(keyjar: KeyJar, filename: str, entity_id) -> KeyJar: -+ with open(filename) as jwks_file: -+ keyjar = import_jwks_as_json(keyjar, jwks_file.read(), entity_id) -+ return keyjar -+ -+ -+def add_kb(keyjar: KeyJar, key_bundle: KeyBundle, entity_id: str) -> KeyJar: -+ return import_jwks(keyjar, json.loads(key_bundle.jwks()), entity_id) -+ -+ -+def add_symmetric(keyjar: KeyJar, key: str, entity_id: Optional[str] = "") -> KeyJar: -+ jar = issuer_keys(keyjar, entity_id) -+ _sym_key = SYMKey(key=key) -+ -+ jwk = _sym_key.serialize() -+ if jwk not in jar: -+ keyjar.add_symmetric(entity_id, key) -+ return keyjar -+ -+ -+def store_under_other_id(keyjar: KeyJar, fro: Optional[str] = "", to: Optional[str] = "", -+ private: Optional[bool] = False) -> KeyJar: -+ if fro == to: -+ return keyjar -+ else: -+ return import_jwks(keyjar, keyjar.export_jwks(private, fro), to) -+ -+ -+def add_keys(keyjar:KeyJar, keys: List[JWK], entity_id) -> KeyJar: -+ _keys = [] -+ jar = issuer_keys(keyjar, entity_id) -+ for key in keys: -+ jwk = key.serialize() -+ if jwk not in jar: -+ jar.append(jwk) -+ _keys.append(key) -+ if _keys: -+ keyjar.add_keys(entity_id, _keys) -+ return keyjar - - -diff --git a/src/idpyoidc/metadata.py b/src/idpyoidc/metadata.py -index 7561d48..e69de29 100644 ---- a/src/idpyoidc/metadata.py -+++ b/src/idpyoidc/metadata.py -@@ -1,274 +0,0 @@ --from functools import cmp_to_key --import logging --from typing import Callable --from typing import Optional -- --from cryptojwt import KeyJar --from cryptojwt.jwe import SUPPORTED --from cryptojwt.jws.jws import SIGNER_ALGS --from cryptojwt.key_jar import init_key_jar --from cryptojwt.utils import importer -- --from idpyoidc.client.util import get_uri --from idpyoidc.impexp import ImpExp --from idpyoidc.util import add_path --from idpyoidc.util import qualified_name -- --logger = logging.getLogger(__name__) -- -- --def metadata_dump(info, exclude_attributes): -- return {qualified_name(info.__class__): info.dump(exclude_attributes=exclude_attributes)} -- -- --def metadata_load(item: dict, **kwargs): -- _class_name = list(item.keys())[0] # there is only one -- _cls = importer(_class_name) -- _cls = _cls().load(item[_class_name]) -- return _cls -- -- --class Metadata(ImpExp): -- parameter = {"prefer": None, "use": None, "callback_path": None, "_local": None} -- -- _supports = {} -- -- def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] = None): -- -- ImpExp.__init__(self) -- if isinstance(prefer, dict): -- self.prefer = {k: v for k, v in prefer.items() if k in self._supports} -- else: -- self.prefer = {} -- -- self.callback_path = callback_path or {} -- self.use = {} -- self._local = {} -- -- def get_use(self): -- return self.use -- -- def set_usage(self, key, value): -- self.use[key] = value -- -- def get_usage(self, key, default=None): -- return self.use.get(key, default) -- -- def get_preference(self, key, default=None): -- return self.prefer.get(key, default) -- -- def set_preference(self, key, value): -- self.prefer[key] = value -- -- def remove_preference(self, key): -- if key in self.prefer: -- del self.prefer[key] -- -- def _callback_uris(self, base_url, hex): -- _uri = [] -- for type in self.get_usage("response_types", self._supports["response_types"]): -- if "code" in type: -- _uri.append("code") -- elif type in ["id_token", "id_token token"]: -- _uri.append("implicit") -- -- if "form_post" in self.supports: -- _uri.append("form_post") -- -- callback_uri = {} -- for key in _uri: -- callback_uri[key] = get_uri(base_url, self.callback_path[key], hex) -- return callback_uri -- -- def construct_redirect_uris(self, base_url: str, hex: str, callbacks: Optional[dict] = None): -- if not callbacks: -- callbacks = self._callback_uris(base_url, hex) -- -- if callbacks: -- self.set_preference("callbacks", callbacks) -- self.set_preference("redirect_uris", [v for k, v in callbacks.items()]) -- -- self.callback = callbacks -- -- def verify_rules(self, supports): -- return True -- -- def locals(self, info): -- pass -- -- def _keyjar(self, keyjar=None, conf=None, entity_id=""): -- _uri_path = "" -- if keyjar is None: -- if "keys" in conf: -- keys_args = {k: v for k, v in conf["keys"].items() if k != "uri_path"} -- _keyjar = init_key_jar(**keys_args) -- _uri_path = conf["keys"].get("uri_path") -- elif "key_conf" in conf and conf["key_conf"]: -- keys_args = {k: v for k, v in conf["key_conf"].items() if k != "uri_path"} -- _keyjar = init_key_jar(**keys_args) -- _uri_path = conf["key_conf"].get("uri_path") -- else: -- _keyjar = KeyJar() -- if "jwks" in conf: -- _keyjar.import_jwks(conf["jwks"], "") -- -- if "" in _keyjar and entity_id: -- # make sure I have the keys under my own name too (if I know it) -- _keyjar.import_jwks_as_json(_keyjar.export_jwks_as_json(True, ""), entity_id) -- -- _httpc_params = conf.get("httpc_params") -- if _httpc_params: -- _keyjar.httpc_params = _httpc_params -- -- return _keyjar, _uri_path -- else: -- if "keys" in conf: -- _uri_path = conf["keys"].get("uri_path") -- elif "key_conf" in conf and conf["key_conf"]: -- _uri_path = conf["key_conf"].get("uri_path") -- return keyjar, _uri_path -- -- def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): -- raise NotImplementedError() -- -- def get_id(self, configuration: dict): -- raise NotImplementedError() -- -- def add_extra_keys(self, keyjar, id): -- return None -- -- def get_jwks(self, keyjar): -- return None -- -- def handle_keys(self, -- configuration: dict, -- keyjar: Optional[KeyJar] = None, -- base_url: Optional[str] = "", -- entity_id: Optional[str] = ""): -- _jwks = _jwks_uri = None -- _id = self.get_id(configuration) -- keyjar, uri_path = self._keyjar(keyjar, configuration, entity_id=_id) -- -- self.add_extra_keys(keyjar, _id) -- -- # now that keys are in the Key Jar, now for how to publish it -- if "jwks_uri" in configuration: # simple -- _jwks_uri = configuration.get("jwks_uri") -- elif uri_path: -- if not base_url: -- base_url = self.get_base_url(configuration, entity_id=entity_id) -- _jwks_uri = add_path(base_url, uri_path) -- else: # jwks or nothing -- _jwks = self.get_jwks(keyjar) -- -- return {"keyjar": keyjar, "jwks": _jwks, "jwks_uri": _jwks_uri} -- -- def load_conf( -- self, configuration, supports, keyjar: Optional[KeyJar] = None, -- base_url: Optional[str] = "" -- ): -- for attr, val in configuration.items(): -- if attr == "preference": -- for k, v in val.items(): -- if k in supports: -- self.set_preference(k, v) -- elif attr in supports: -- self.set_preference(attr, val) -- -- self.locals(configuration) -- -- for key, val in self.handle_keys(configuration, keyjar=keyjar, base_url=base_url).items(): -- if key == "keyjar": -- keyjar = val -- elif val: -- self.set_preference(key, val) -- -- self.verify_rules(supports) -- return keyjar -- -- def get(self, key, default=None): -- if key in self._local: -- return self._local[key] -- else: -- return default -- -- def set(self, key, val): -- self._local[key] = val -- -- def construct_uris(self, *args): -- pass -- -- def supports(self): -- res = {} -- for key, val in self._supports.items(): -- if isinstance(val, Callable): -- res[key] = val() -- else: -- res[key] = val -- return res -- -- def supported(self, claim): -- return claim in self._supports -- -- def prefers(self): -- return self.prefer -- -- --SIGNING_ALGORITHM_SORT_ORDER = ["RS", "ES", "PS", "HS", "Ed"] -- -- --def cmp(a, b): -- return (a > b) - (a < b) -- -- --def alg_cmp(a, b): -- if a == "none": -- return 1 -- elif b == "none": -- return -1 -- -- _pos1 = SIGNING_ALGORITHM_SORT_ORDER.index(a[0:2]) -- _pos2 = SIGNING_ALGORITHM_SORT_ORDER.index(b[0:2]) -- if _pos1 == _pos2: -- return (a > b) - (a < b) -- elif _pos1 > _pos2: -- return 1 -- else: -- return -1 -- -- --def get_signing_algs(): -- # Assumes Cryptojwt -- _algs = [name for name in list(SIGNER_ALGS.keys()) if name != "none"] -- return sorted(_algs, key=cmp_to_key(alg_cmp)) -- -- --def get_encryption_algs(): -- return SUPPORTED["alg"] -- -- --def get_encryption_encs(): -- return SUPPORTED["enc"] -- -- --def array_or_singleton(claim_spec, values): -- if isinstance(claim_spec[0], list): -- if isinstance(values, list): -- return values -- else: -- return [values] -- else: -- if isinstance(values, list): -- return values[0] -- else: # singleton -- return values -- -- --def is_subset(a, b): -- if isinstance(a, list): -- if isinstance(b, list): -- return set(b).issubset(set(a)) -- elif isinstance(b, list): -- return a in b -- else: -- return a == b - -diff --git a/src/idpyoidc/node.py b/src/idpyoidc/node.py -index 0db622a..2b64d6e 100644 ---- a/src/idpyoidc/node.py -+++ b/src/idpyoidc/node.py -@@ -7,14 +7,17 @@ from cryptojwt.key_jar import init_key_jar - - from idpyoidc.configure import Configuration - from idpyoidc.impexp import ImpExp -+from idpyoidc.key_import import import_jwks -+from idpyoidc.key_import import import_jwks_as_json -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.util import instantiate - - - def create_keyjar( -- keyjar: Optional[KeyJar] = None, -- conf: Optional[Union[dict, Configuration]] = None, -- key_conf: Optional[dict] = None, -- id: Optional[str] = "", -+ keyjar: Optional[KeyJar] = None, -+ conf: Optional[Union[dict, Configuration]] = None, -+ key_conf: Optional[dict] = None, -+ id: Optional[str] = "", - ): - if keyjar is None: - if key_conf: -@@ -30,13 +33,13 @@ def create_keyjar( - else: - _keyjar = KeyJar() - if "jwks" in conf: -- _keyjar.import_jwks(conf["jwks"], "") -+ _keyjar = import_jwks(_keyjar, conf["jwks"], "") - else: - _keyjar = None - - if _keyjar and "" in _keyjar and id: - # make sure I have the keys under my own name too (if I know it) -- _keyjar.import_jwks_as_json(_keyjar.export_jwks_as_json(True, ""), id) -+ _keyjar = store_under_other_id(_keyjar, "", id, True) - - return _keyjar - else: -@@ -60,7 +63,7 @@ def make_keyjar( - keyjar = KeyJar() - _jwks = config.get("jwks") - if _jwks: -- keyjar.import_jwks_as_json(_jwks, client_id) -+ keyjar = import_jwks_as_json(keyjar, _jwks, client_id) - - if keyjar or key_conf: - # Should be either one -@@ -78,15 +81,12 @@ def make_keyjar( - keyjar = KeyJar() - keyjar.add_symmetric(client_id, _key) - keyjar.add_symmetric("", _key) -- # else: -- # keyjar = build_keyjar(DEFAULT_KEY_DEFS) -- # if issuer_id: -- # keyjar.import_jwks(keyjar.export_jwks(private=True), issuer_id) - - return keyjar - - - class Node: -+ - def __init__(self, upstream_get: Callable = None): - self.upstream_get = upstream_get - -@@ -123,19 +123,20 @@ class Unit(ImpExp): - init_args = ["upstream_get"] - - def __init__( -- self, -- upstream_get: Callable = None, -- keyjar: Optional[Union[KeyJar, bool]] = None, -- httpc: Optional[object] = None, -- httpc_params: Optional[dict] = None, -- config: Optional[Union[Configuration, dict]] = None, -- key_conf: Optional[dict] = None, -- issuer_id: Optional[str] = "", -- client_id: Optional[str] = "", -+ self, -+ upstream_get: Callable = None, -+ keyjar: Optional[Union[KeyJar, bool]] = None, -+ httpc: Optional[object] = None, -+ httpc_params: Optional[dict] = None, -+ config: Optional[Union[Configuration, dict]] = None, -+ key_conf: Optional[dict] = None, -+ issuer_id: Optional[str] = "", -+ client_id: Optional[str] = "", - ): - ImpExp.__init__(self) - self.upstream_get = upstream_get - self.httpc = httpc -+ self.client_id = client_id - - if config is None: - config = {} -@@ -192,16 +193,16 @@ class ClientUnit(Unit): - name = "" - - def __init__( -- self, -- upstream_get: Callable = None, -- httpc: Optional[object] = None, -- httpc_params: Optional[dict] = None, -- keyjar: Optional[KeyJar] = None, -- context: Optional[ImpExp] = None, -- config: Optional[Union[Configuration, dict]] = None, -- # jwks_uri: Optional[str] = "", -- entity_id: Optional[str] = "", -- key_conf: Optional[dict] = None, -+ self, -+ upstream_get: Callable = None, -+ httpc: Optional[object] = None, -+ httpc_params: Optional[dict] = None, -+ keyjar: Optional[KeyJar] = None, -+ context: Optional[ImpExp] = None, -+ config: Optional[Union[Configuration, dict]] = None, -+ # jwks_uri: Optional[str] = "", -+ entity_id: Optional[str] = "", -+ key_conf: Optional[dict] = None, - ): - if config is None: - config = {} -@@ -232,17 +233,18 @@ class ClientUnit(Unit): - - # Neither client nor Server - class Collection(Unit): -+ - def __init__( -- self, -- upstream_get: Callable = None, -- keyjar: Optional[KeyJar] = None, -- httpc: Optional[object] = None, -- httpc_params: Optional[dict] = None, -- config: Optional[Union[Configuration, dict]] = None, -- entity_id: Optional[str] = "", -- key_conf: Optional[dict] = None, -- functions: Optional[dict] = None, -- claims: Optional[dict] = None, -+ self, -+ upstream_get: Callable = None, -+ keyjar: Optional[KeyJar] = None, -+ httpc: Optional[object] = None, -+ httpc_params: Optional[dict] = None, -+ config: Optional[Union[Configuration, dict]] = None, -+ entity_id: Optional[str] = "", -+ key_conf: Optional[dict] = None, -+ functions: Optional[dict] = None, -+ claims: Optional[dict] = None, - ): - if config is None: - config = {} - diff --git a/patch/claims.patch b/patch/claims.patch deleted file mode 100644 index b0c2cd45..00000000 --- a/patch/claims.patch +++ /dev/null @@ -1,1370 +0,0 @@ - -diff --git a/src/idpyoidc/client/claims/__init__.py b/src/idpyoidc/client/claims/__init__.py -index 1427005..a13f25d 100644 ---- a/src/idpyoidc/client/claims/__init__.py -+++ b/src/idpyoidc/client/claims/__init__.py -@@ -13,6 +13,8 @@ def get_client_authn_methods(): - - - class Claims(claims.Claims): -+ _supports = {} -+ - def get_base_url(self, configuration: dict, entity_id: Optional[str] = ""): - _base = configuration.get("base_url") - if not _base: -@@ -56,7 +58,7 @@ class Claims(claims.Claims): - if ( - len(_own_keys) == 1 - and isinstance(_own_keys[0], SYMKey) -- and self.prefer["client_secret"] -+ and self.prefer.get("client_secret", None) - ): - pass - else: -diff --git a/src/idpyoidc/client/claims/oauth2.py b/src/idpyoidc/client/claims/oauth2.py -index 9d093d4..f4cf8d9 100644 ---- a/src/idpyoidc/client/claims/oauth2.py -+++ b/src/idpyoidc/client/claims/oauth2.py -@@ -1,21 +1,38 @@ - from typing import Optional - - from idpyoidc.client import claims --from idpyoidc.client.claims.transform import create_registration_request -+from idpyoidc.transform import create_registration_request -+ -+REGISTER2PREFERRED = { -+ "scope": "scopes_supported", -+ "token_endpoint_auth_signing_alg": "token_endpoint_auth_signing_alg_values_supported", -+ "response_types": "response_types_supported", -+ # "response_modes": "response_modes_supported", -+ "grant_types": "grant_types_supported", -+ "token_endpoint_auth_method": "token_endpoint_auth_methods_supported", -+ "token_auth_signing_algs": "token_auth_signing_algs_supported", -+ # 'ui_locales': 'ui_locales_supported', -+} - - - class Claims(claims.Claims): -+ register2preferred = REGISTER2PREFERRED -+ - _supports = { - "redirect_uris": None, -- "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], -+ # "scopes_supported": [], - "response_types_supported": ["code"], -+ # "response_modes_supported": ["query", "fragment"], -+ "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], -+ "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"], -+ # "token_auth_signing_algs_supported": metadata.get_signing_algs(), - "client_id": None, -- "client_secret": None, - "client_name": None, -+ "client_secret": None, - "client_uri": None, - "logo_uri": None, -+ "scope": None, - "contacts": None, -- "scopes_supported": [], - "tos_uri": None, - "policy_uri": None, - "jwks_uri": None, - - -diff --git a/src/idpyoidc/client/claims/oauth2resource.py b/src/idpyoidc/client/claims/oauth2resource.py -index 537e139..2bec3c8 100644 ---- a/src/idpyoidc/client/claims/oauth2resource.py -+++ b/src/idpyoidc/client/claims/oauth2resource.py -@@ -2,7 +2,7 @@ from typing import Optional - - from idpyoidc.client import claims - from idpyoidc.message.oauth2 import OAuthProtectedResourceRequest --from idpyoidc.client.claims.transform import array_or_singleton -+from idpyoidc.transform import array_or_singleton - - class Claims(claims.Claims): - _supports = { - - - -diff --git a/src/idpyoidc/client/claims/oidc.py b/src/idpyoidc/client/claims/oidc.py -index 0529f16..d8ae08b 100644 ---- a/src/idpyoidc/client/claims/oidc.py -+++ b/src/idpyoidc/client/claims/oidc.py -@@ -2,9 +2,9 @@ import logging - import os - from typing import Optional - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.client import claims as client_claims --from idpyoidc.client.claims.transform import create_registration_request -+from idpyoidc.transform import create_registration_request - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - from idpyoidc.message.oidc import RegistrationRequest - from idpyoidc.message.oidc import RegistrationResponse -@@ -71,14 +71,15 @@ class Claims(client_claims.Claims): - "client_name": None, - "client_secret": None, - "client_uri": None, -+ "code_challenge_methods_supported": None, - "contacts": None, - "default_max_age": 86400, - "encrypt_id_token_supported": None, - # "grant_types_supported": ["authorization_code", "refresh_token"], - "logo_uri": None, -- "id_token_signing_alg_values_supported": metadata.get_signing_algs(), -- "id_token_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "id_token_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "id_token_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "id_token_encryption_alg_values_supported": alg_info.get_encryption_algs(), -+ "id_token_encryption_enc_values_supported": alg_info.get_encryption_encs(), - "initiate_login_uri": None, - "jwks": None, - "jwks_uri": None, -@@ -95,13 +96,13 @@ class Claims(client_claims.Claims): - client_claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) - - def verify_rules(self, supports): -- if self.get_preference("request_parameter_supported") and self.get_preference( -- "request_uri_parameter_supported" -- ): -- raise ValueError( -- "You have to chose one of 'request_parameter_supported' and " -- "'request_uri_parameter_supported'. You can't have both." -- ) -+ # if self.get_preference("request_parameter_supported") and self.get_preference( -+ # "request_uri_parameter_supported" -+ # ): -+ # raise ValueError( -+ # "You have to chose one of 'request_parameter_supported' and " -+ # "'request_uri_parameter_supported'. You can't have both." -+ # ) - - if self.get_preference("request_parameter_supported") or self.get_preference( - "request_uri_parameter_supported" - -diff --git a/src/idpyoidc/server/__init__.py b/src/idpyoidc/server/__init__.py -index 78c2370..9657257 100644 ---- a/src/idpyoidc/server/__init__.py -+++ b/src/idpyoidc/server/__init__.py -@@ -6,6 +6,7 @@ from typing import Optional - from typing import Union - - from cryptojwt import KeyJar -+from cryptojwt.utils import importer - - from idpyoidc.client.defaults import DEFAULT_KEY_DEFS - from idpyoidc.node import Unit -@@ -52,6 +53,9 @@ class Server(Unit): - if _conf: - self.entity_id = _conf.get("entity_id", "") - self.issuer = conf.get("issuer", self.entity_id) -+ if not self.entity_id and self.issuer: -+ self.entity_id = self.issuer -+ - self.persistence = None - - if upstream_get is None: -@@ -95,6 +99,19 @@ class Server(Unit): - - _token_endp = self.endpoint.get("token") - -+ if isinstance(conf, dict): -+ metadata_schema = conf.get("metadata_schema", None) -+ else: -+ metadata_schema = conf.conf.get("metadata_schema", None) -+ if metadata_schema: -+ metadata_schema = importer(metadata_schema) -+ self.context.provider_info = self.context.claims.get_server_metadata( -+ endpoints=self.endpoint.values(), -+ metadata_schema=metadata_schema, -+ ) -+ self.context.provider_info["issuer"] = self.issuer -+ self.context.metadata = self.context.provider_info -+ - self.context.map_supported_to_preferred() - if _token_endp: - _token_endp.allow_refresh = allow_refresh_token(self.context) -diff --git a/src/idpyoidc/server/claims/oauth2.py b/src/idpyoidc/server/claims/oauth2.py -index 86e969d..243e09b 100644 ---- a/src/idpyoidc/server/claims/oauth2.py -+++ b/src/idpyoidc/server/claims/oauth2.py -@@ -1,5 +1,6 @@ - from typing import Optional - -+from idpyoidc.message import Message - from idpyoidc.message.oauth2 import ASConfigurationResponse - from idpyoidc.server import claims - -@@ -38,9 +39,12 @@ class Claims(claims.Claims): - def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] = None): - claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) - -- def provider_info(self, supports): -+ def metadata(self, supports, schema: Optional[Message] = None): - _info = {} -- for key in ASConfigurationResponse.c_param.keys(): -+ if schema is None: -+ schema = ASConfigurationResponse -+ -+ for key in schema.c_param.keys(): - _val = self.get_preference(key, supports.get(key, None)) - if _val and _val != []: - _info[key] = _val -diff --git a/src/idpyoidc/server/claims/oidc.py b/src/idpyoidc/server/claims/oidc.py -index 2c258ba..0646410 100644 ---- a/src/idpyoidc/server/claims/oidc.py -+++ b/src/idpyoidc/server/claims/oidc.py -@@ -1,6 +1,7 @@ - from typing import Optional - --from idpyoidc import metadata -+from idpyoidc import alg_info -+from idpyoidc.message import Message - from idpyoidc.message.oidc import ProviderConfigurationResponse - from idpyoidc.message.oidc import RegistrationRequest - from idpyoidc.message.oidc import RegistrationResponse -@@ -48,9 +49,9 @@ class Claims(server_claims.Claims): - "display_values_supported": None, - "encrypt_id_token_supported": None, - # "grant_types_supported": ["authorization_code", "implicit", "refresh_token"], -- "id_token_signing_alg_values_supported": metadata.get_signing_algs(), -- "id_token_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "id_token_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "id_token_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "id_token_encryption_alg_values_supported": alg_info.get_encryption_algs(), -+ "id_token_encryption_enc_values_supported": alg_info.get_encryption_encs(), - "initiate_login_uri": None, - "jwks": None, - "jwks_uri": None, -@@ -71,13 +72,13 @@ class Claims(server_claims.Claims): - server_claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path) - - def verify_rules(self, supports): -- if self.get_preference("request_parameter_supported") and self.get_preference( -- "request_uri_parameter_supported" -- ): -- raise ValueError( -- "You have to chose one of 'request_parameter_supported' and " -- "'request_uri_parameter_supported'. You can't have both." -- ) -+ # if self.get_preference("request_parameter_supported") and self.get_preference( -+ # "request_uri_parameter_supported" -+ # ): -+ # raise ValueError( -+ # "You have to chose one of 'request_parameter_supported' and " -+ # "'request_uri_parameter_supported'. You can't have both." -+ # ) - - if not self.get_preference("encrypt_userinfo_supported"): - self.set_preference("userinfo_encryption_alg_values_supported", []) -@@ -91,7 +92,7 @@ class Claims(server_claims.Claims): - self.set_preference("id_token_encryption_alg_values_supported", []) - self.set_preference("id_token_encryption_enc_values_supported", []) - -- def provider_info(self, supports): -+ def provider_info(self, supports, schema: Optional[Message] = None): - _info = {} - for key in ProviderConfigurationResponse.c_param.keys(): - _val = self.get_preference(key, supports.get(key, None)) - - -diff --git a/src/idpyoidc/server/configure.py b/src/idpyoidc/server/configure.py -index 3f304e7..bb4b507 100755 ---- a/src/idpyoidc/server/configure.py -+++ b/src/idpyoidc/server/configure.py -@@ -83,7 +83,7 @@ _C = { - "client_authn_method": None, - "claims_parameter_supported": True, - "request_parameter_supported": True, -- "request_uri_parameter_supported": True, -+ "request_uri_parameter_supported": None, - "response_types_supported": ["code"], - "response_modes_supported": ["query", "fragment", "form_post"], - }, -@@ -152,7 +152,7 @@ OP_DEFAULT_CONFIG.update( - "client_authn_method": None, - "claims_parameter_supported": True, - "request_parameter_supported": True, -- "request_uri_parameter_supported": True, -+ "request_uri_parameter_supported": None, - "response_types_supported": [ - "code", - # "token", -@@ -480,7 +480,7 @@ DEFAULT_EXTENDED_CONF = { - "client_authn_method": None, - "claims_parameter_supported": True, - "request_parameter_supported": True, -- "request_uri_parameter_supported": True, -+ "request_uri_parameter_supported": None, - "response_types_supported": [ - "code", - # "token", -diff --git a/src/idpyoidc/server/endpoint.py b/src/idpyoidc/server/endpoint.py -index 07c0080..56074ad 100755 ---- a/src/idpyoidc/server/endpoint.py -+++ b/src/idpyoidc/server/endpoint.py -@@ -181,11 +181,11 @@ class Endpoint(Node): - return None - - def parse_request( -- self, -- request: Union[Message, dict, str], -- http_info: Optional[dict] = None, -- verify_args: Optional[dict] = None, -- **kwargs -+ self, -+ request: Union[Message, dict, str], -+ http_info: Optional[dict] = None, -+ verify_args: Optional[dict] = None, -+ **kwargs - ): - """ - -@@ -196,7 +196,9 @@ class Endpoint(Node): - :return: - """ - LOGGER.debug("- {} -".format(self.endpoint_name)) -- LOGGER.info("Request: %s" % sanitize(request)) -+ LOGGER.info(f"Request: {sanitize(request)}") -+ if http_info: -+ LOGGER.info(f"HTTP info: {http_info}") - - _context = self.upstream_get("context") - _keyjar = self.upstream_get("attribute", "keyjar") -@@ -240,8 +242,6 @@ class Endpoint(Node): - else: - _client_id = req.get("client_id", None) - -- LOGGER.debug(f"parse_request:auth_info:{auth_info}") -- - # verify that the request message is correct, may have to do it twice - err_response = self.verify_request( - request=req, keyjar=_keyjar, client_id=_client_id, verify_args=verify_args -@@ -274,17 +274,18 @@ class Endpoint(Node): - - authn_info = verify_client(request=request, http_info=http_info, **kwargs) - -- LOGGER.debug("authn_info: %s", authn_info) -+ LOGGER.debug(f"authn_info: {authn_info}") - if authn_info == {}: - if self.client_authn_method and len(self.client_authn_method): -- LOGGER.debug("client_authn_method: %s", self.client_authn_method) -+ LOGGER.debug(f"client_authn_method: {self.client_authn_method}") - raise UnAuthorizedClient("Authorization failed") - elif "client_id" not in authn_info and authn_info.get("method") != "none": -+ LOGGER.debug(f"No client ID") - raise UnAuthorizedClient("Authorization failed") - return authn_info - - def do_post_parse_request( -- self, request: Message, client_id: Optional[str] = "", **kwargs -+ self, request: Message, client_id: Optional[str] = "", **kwargs - ) -> Message: - _context = self.upstream_get("context") - for meth in self.post_parse_request: -@@ -294,7 +295,7 @@ class Endpoint(Node): - return request - - def do_pre_construct( -- self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs -+ self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs - ) -> dict: - _context = self.upstream_get("context") - for meth in self.pre_construct: -@@ -303,10 +304,10 @@ class Endpoint(Node): - return response_args - - def do_post_construct( -- self, -- response_args: Union[Message, dict], -- request: Optional[Union[Message, dict]] = None, -- **kwargs -+ self, -+ response_args: Union[Message, dict], -+ request: Optional[Union[Message, dict]] = None, -+ **kwargs - ) -> dict: - _context = self.upstream_get("context") - for meth in self.post_construct: -@@ -315,10 +316,10 @@ class Endpoint(Node): - return response_args - - def process_request( -- self, -- request: Optional[Union[Message, dict]] = None, -- http_info: Optional[dict] = None, -- **kwargs -+ self, -+ request: Optional[Union[Message, dict]] = None, -+ http_info: Optional[dict] = None, -+ **kwargs - ) -> Union[Message, dict]: - """ - -@@ -329,10 +330,10 @@ class Endpoint(Node): - return {} - - def construct( -- self, -- response_args: Optional[dict] = None, -- request: Optional[Union[Message, dict]] = None, -- **kwargs -+ self, -+ response_args: Optional[dict] = None, -+ request: Optional[Union[Message, dict]] = None, -+ **kwargs - ): - """ - Construct the response -@@ -350,19 +351,34 @@ class Endpoint(Node): - return self.do_post_construct(response, request, **kwargs) - - def response_info( -- self, -- response_args: Optional[dict] = None, -- request: Optional[Union[Message, dict]] = None, -- **kwargs -+ self, -+ response_args: Optional[dict] = None, -+ request: Optional[Union[Message, dict]] = None, -+ **kwargs - ) -> dict: - return self.construct(response_args, request, **kwargs) - -+ def _get_content_type(self, **kwargs): -+ content_type = kwargs.get("content_type", None) -+ if content_type is None: -+ if self.response_content_type: -+ content_type = self.response_content_type -+ elif self.response_format == "json": -+ content_type = "application/json" -+ elif self.response_format in ["jws", "jwe", "jose"]: -+ content_type = "application/jose" -+ elif self.response_format == "text": -+ content_type = "text/plain" -+ else: -+ content_type = "application/x-www-form-urlencoded" -+ return content_type -+ - def do_response( -- self, -- response_args: Optional[dict] = None, -- request: Optional[Union[Message, dict]] = None, -- error: Optional[str] = "", -- **kwargs -+ self, -+ response_args: Optional[dict] = None, -+ request: Optional[Union[Message, dict]] = None, -+ error: Optional[str] = "", -+ **kwargs - ) -> dict: - """ - :param response_args: Information to use when constructing the response -@@ -370,7 +386,6 @@ class Endpoint(Node): - :param error: Possible error encountered while processing the request - """ - do_placement = True -- content_type = "text/html" - _resp = {} - _response_placement = None - if response_args is None: -@@ -380,6 +395,7 @@ class Endpoint(Node): - - resp = None - if error: -+ content_type = "text/html" - _response = ResponseMessage(error=error) - for attr in ["error_description", "error_uri", "state"]: - if attr in kwargs: -@@ -389,58 +405,50 @@ class Endpoint(Node): - _response_placement = kwargs.get("response_placement") - do_placement = False - _response = "" -- content_type = kwargs.get("content_type") -- if content_type is None: -- if self.response_content_type: -- content_type = self.response_content_type -- elif self.response_format == "json": -- content_type = "application/json" -- elif self.response_format in ["jws", "jwe", "jose"]: -- content_type = "application/jose" -- elif self.response_format == "text": -- content_type = "text/plain" -- else: -- content_type = "application/x-www-form-urlencoded" -+ content_type = self._get_content_type(**kwargs) - else: -+ content_type = "" - _response = self.response_info(response_args, request, **kwargs) - - if do_placement: -- content_type = kwargs.get("content_type") -- if content_type is None: -- if self.response_placement == "body": -- if self.response_format == "json": -+ if not content_type: -+ content_type = self._get_content_type(**kwargs) -+ -+ if self.response_placement == "body": -+ if self.response_format == "json": -+ if not content_type: - content_type = "application/json; charset=utf-8" -- if isinstance(_response, Message): -- resp = _response.to_json() -- else: -- resp = json.dumps(_response) -- elif self.response_format in ["jws", "jwe", "jose"]: -- if self.response_content_type: -- content_type = self.response_content_type -- else: -- content_type = "application/jose; charset=utf-8" -- resp = _response -+ if isinstance(_response, Message): -+ resp = _response.to_json() - else: -+ resp = json.dumps(_response) -+ elif self.response_format in ["jws", "jwe", "jose"]: -+ if not content_type: -+ content_type = "application/jose; charset=utf-8" -+ resp = _response -+ else: -+ if not content_type: - content_type = "application/x-www-form-urlencoded" -- resp = _response.to_urlencoded() -- elif self.response_placement == "url": -+ resp = _response.to_urlencoded() -+ elif self.response_placement == "url": -+ if not content_type: - content_type = "application/x-www-form-urlencoded" -- fragment_enc = kwargs.get("fragment_enc") -- if not fragment_enc: -- _ret_type = kwargs.get("return_type") -- if _ret_type: -- fragment_enc = fragment_encoding(_ret_type) -- else: -- fragment_enc = False -- -- if fragment_enc: -- resp = _response.request(kwargs["return_uri"], True) -+ fragment_enc = kwargs.get("fragment_enc") -+ if not fragment_enc: -+ _ret_type = kwargs.get("return_type") -+ if _ret_type: -+ fragment_enc = fragment_encoding(_ret_type) - else: -- resp = _response.request(kwargs["return_uri"]) -+ fragment_enc = False -+ -+ if fragment_enc: -+ resp = _response.request(kwargs["return_uri"], True) - else: -- raise ValueError( -- "Don't know where that is: '{}".format(self.response_placement) -- ) -+ resp = _response.request(kwargs["return_uri"]) -+ else: -+ raise ValueError( -+ "Don't know where that is: '{}".format(self.response_placement) -+ ) - - if content_type: - try: -diff --git a/src/idpyoidc/server/endpoint_context.py b/src/idpyoidc/server/endpoint_context.py -index 3b46ef3..ac21db6 100755 ---- a/src/idpyoidc/server/endpoint_context.py -+++ b/src/idpyoidc/server/endpoint_context.py -@@ -11,6 +11,7 @@ from jinja2 import FileSystemLoader - from requests import request - - from idpyoidc.context import OidcContext -+from idpyoidc.message import Message - from idpyoidc.server import authz - from idpyoidc.server.claims import Claims - from idpyoidc.server.claims.oauth2 import Claims as OAUTH2_Claims -@@ -173,6 +174,7 @@ class EndpointContext(OidcContext): - self.token_args_methods = [] - self.userinfo = None - self.client_authn_method = {} -+ self.client_known_as = {} - - for param in [ - "issuer", -@@ -186,8 +188,6 @@ class EndpointContext(OidcContext): - except KeyError: - pass - -- self.token_handler_args = get_token_handler_args(conf) -- - # session db - self._sub_func = {} - self.do_sub_func() -@@ -240,9 +240,6 @@ class EndpointContext(OidcContext): - conf = conf.conf - _supports = self.supports() - self.keyjar = self.claims.load_conf(conf, supports=_supports, keyjar=keyjar) -- self.provider_info = self.claims.provider_info(_supports) -- self.provider_info["issuer"] = self.issuer -- self.provider_info.update(self._get_endpoint_info()) - - # INTERFACES - -@@ -250,23 +247,33 @@ class EndpointContext(OidcContext): - - self.setup_authentication() - -- self.session_manager = SessionManager( -- self.token_handler_args, -- sub_func=self._sub_func, -- conf=conf, -- upstream_get=self.unit_get) -+ # default is to have session management -+ if self.conf.get("session_management", self.conf["conf"].get("session_management", True)): -+ self.token_handler_args = get_token_handler_args(self.conf) -+ -+ self.session_manager = SessionManager( -+ self.token_handler_args, -+ sub_func=self._sub_func, -+ conf=conf, -+ upstream_get=self.unit_get) -+ else: -+ self.session_manager = None - - self.do_userinfo() - - # Must be done after userinfo - self.setup_login_hint_lookup() -- self.set_remember_token() -+ if self.session_manager: -+ self.set_remember_token() - - self.setup_client_authn_methods() - -- # _id_token_handler = self.session_manager.token_handler.handler.get("id_token") -- # if _id_token_handler: -- # self.provider_info.update(_id_token_handler.provider_info) -+ def get_metadata(self, supports: Optional[dict] = None, schema: Optional[Message] = None): -+ if supports is None: -+ supports = self.supports() -+ _metadata = self.claims.metadata(supports, schema) -+ _metadata.update(self._get_endpoint_info()) -+ return _metadata - - def setup_authz(self): - authz_spec = self.conf.get("authz") - -diff --git a/src/idpyoidc/server/session/grant.py b/src/idpyoidc/server/session/grant.py -index d7ee7c3..53d101c 100644 ---- a/src/idpyoidc/server/session/grant.py -+++ b/src/idpyoidc/server/session/grant.py -@@ -377,11 +377,10 @@ class Grant(Item): - ) - - logger.debug(f"token_payload: {token_payload}") -- - item.value = token_handler( - session_id=session_id, usage_rules=usage_rules, **token_payload - ) -- -+ logger.debug(f"token: {item.value}") - if based_on: - based_on.used += 1 - else: -diff --git a/src/idpyoidc/server/session/manager.py b/src/idpyoidc/server/session/manager.py -index ddd5a9d..bb38340 100644 ---- a/src/idpyoidc/server/session/manager.py -+++ b/src/idpyoidc/server/session/manager.py -@@ -1,10 +1,12 @@ - import hashlib - import logging - import os --import uuid - from typing import Callable - from typing import List - from typing import Optional -+import uuid -+ -+from cryptojwt.jwe.fernet import FernetEncrypter - - from idpyoidc.encrypter import default_crypt_config - from idpyoidc.message.oauth2 import AuthorizationRequest -@@ -20,9 +22,9 @@ from .grant import Grant - from .grant import SessionToken - from .info import ClientSessionInfo - from .info import UserSessionInfo --from ..token import handler - from ..token import UnknownToken - from ..token import WrongTokenClass -+from ..token import handler - from ..token.handler import TokenHandler - - logger = logging.getLogger(__name__) -@@ -84,6 +86,7 @@ def ephemeral_id(*args, **kwargs): - - class SessionManager(GrantManager): - parameter = Database.parameter.copy() -+ - # parameter.update({"salt": ""}) - init_args = ["token_handler_args", "upstream_get"] - -@@ -437,30 +440,6 @@ class SessionManager(GrantManager): - """ - self._revoke_tree(self.get_grant(session_id)) - -- # def grants( -- # self, -- # session_id: Optional[str] = "", -- # user_id: Optional[str] = "", -- # client_id: Optional[str] = "", -- # ) -> List[Grant]: -- # """ -- # Find all grant connected to a user session -- # -- # :param client_id: -- # :param user_id: -- # :param session_id: A session identifier -- # :return: A list of grants -- # """ -- # if session_id: -- # user_id, client_id, _ = self.decrypt_session_id(session_id) -- # elif user_id and client_id: -- # pass -- # else: -- # raise AttributeError("Must have session_id or user_id and client_id") -- # -- # _csi = self.get([user_id, client_id]) -- # return [self.get([user_id, client_id, gid]) for gid in _csi.subordinate] -- - def get_session_info( - self, - session_id: str, -@@ -551,5 +530,15 @@ class SessionManager(GrantManager): - def unpack_session_key(self, key): - return self.unpack_branch_key(key) - --# def create_session_manager(upstream_get, token_handler_args, sub_func=None, conf=None): --# return SessionManager(token_handler_args, sub_func=sub_func, conf=conf, upstream_get=upstream_get) -+ def get_client_id_from_token(self, token_value: str, handler_key: Optional[str] = ""): -+ if handler_key: -+ _token_info = self.token_handler.handler[handler_key].info(token_value) -+ else: -+ _token_info = self.token_handler.info(token_value) -+ -+ sid = _token_info.get("sid") -+ _path = self.decrypt_branch_id(sid) -+ if len(_path) == 3: -+ return _path[1] -+ else: -+ return _path[-1] -diff --git a/src/idpyoidc/server/token/__init__.py b/src/idpyoidc/server/token/__init__.py -index 8c92e56..d7a8a2a 100755 ---- a/src/idpyoidc/server/token/__init__.py -+++ b/src/idpyoidc/server/token/__init__.py -@@ -9,7 +9,6 @@ from idpyoidc.server.util import lv_pack - from idpyoidc.server.util import lv_unpack - from idpyoidc.time_util import utc_time_sans_frac - from idpyoidc.util import rndstr -- - from .exception import UnknownToken - from .exception import WrongTokenClass - -@@ -92,7 +91,7 @@ class DefaultToken(Token): - self.token_type = token_type - - def __call__( -- self, session_id: Optional[str] = "", token_class: Optional[str] = "", **payload -+ self, session_id: Optional[str] = "", token_class: Optional[str] = "", **payload - ) -> str: - """ - Return a token. -@@ -105,23 +104,40 @@ class DefaultToken(Token): - else: - token_class = "authorization_code" - -+ logger.debug(f"Mint {token_class}") -+ logger.debug(f"crypt.key: {self.crypt.key}") -+ _jwks = self.crypt_config.get('jwks', None) -+ logger.debug(f"crypt.jwks: {_jwks}") -+ - if self.lifetime >= 0: - exp = str(utc_time_sans_frac() + self.lifetime) - else: -- exp = "-1" # Live for ever -+ exp = "-1" # Live forever - - tmp = "" - rnd = "" - while rnd == tmp: # Don't use the same random value again - rnd = rndstr(32) # Ultimate length multiple of 16 - -- return base64.b64encode( -+ _args = { -+ "rnd": rnd, -+ "token_class": token_class, -+ "session_id": session_id, -+ "exp": exp -+ } -+ logger.debug(f"Encrypt arguments: {_args}") -+ _value = base64.urlsafe_b64encode( - self.crypt.encrypt(lv_pack(rnd, token_class, session_id, exp).encode()) - ).decode("utf-8") - -+ logger.debug(f"Token: {_value}") -+ return _value -+ - def split_token(self, token): -+ logger.debug(f"split_token: {token}") -+ logger.debug(f"crypt key: {self.crypt.key}") - try: -- plain = self.crypt.decrypt(base64.b64decode(token)) -+ plain = self.crypt.decrypt(base64.urlsafe_b64decode(token)) - except Exception as err: - raise UnknownToken(err) - # order: rnd, type, sid -diff --git a/src/idpyoidc/server/token/handler.py b/src/idpyoidc/server/token/handler.py -index 8fa9063..6289812 100755 ---- a/src/idpyoidc/server/token/handler.py -+++ b/src/idpyoidc/server/token/handler.py -@@ -137,6 +137,8 @@ def default_token(spec): - else: - return False - -+def key_types(keys): -+ return [k["kid"] for k in keys] - - JWKS_FILE = "private/token_jwks.json" - -@@ -192,10 +194,18 @@ def factory( - ("token", token, "access_token"), - ("refresh", refresh, "refresh_token"), - ]: -- if cnf is not None: -- if default_token(cnf): -- if kj: -- _add_passwd(kj, cnf, cls) -+ if cnf is not None: # else just default -+ try: -+ _key_types = key_types(cnf["kwargs"]["crypt_conf"]["kwargs"]["keys"]["key_defs"]) -+ except KeyError: # will fail on keys if it fails -+ pass -+ else: -+ if "key" in _key_types and "password" in _key_types: -+ raise ValueError("You have to chose one of key or password") -+ if "password" not in _key_types and "key" not in _key_types: -+ if kj: -+ _add_passwd(kj, cnf, cls) -+ logger.debug(f"init_token_handler: {cls}") - args[attr] = init_token_handler(upstream_get, cnf, token_class_map[cls]) - - if id_token is not None: -diff --git a/src/idpyoidc/server/token/jwt_token.py b/src/idpyoidc/server/token/jwt_token.py -index abbfa97..534bb49 100644 ---- a/src/idpyoidc/server/token/jwt_token.py -+++ b/src/idpyoidc/server/token/jwt_token.py -@@ -1,3 +1,4 @@ -+import logging - from typing import Callable - from typing import Optional - from typing import Union -@@ -7,30 +8,32 @@ from cryptojwt.jws.exception import JWSException - from cryptojwt.utils import importer - - from idpyoidc.server.exception import ToOld -- --from ...message import Message --from ...message.oauth2 import JWTAccessToken --from ..constant import DEFAULT_TOKEN_LIFETIME --from . import Token - from . import is_expired -+from . import Token - from .exception import UnknownToken - from .exception import WrongTokenClass -+from ..constant import DEFAULT_TOKEN_LIFETIME -+from ...message import Message -+from ...message.oauth2 import JWTAccessToken -+ -+logger = logging.getLogger(__name__) - - - class JWTToken(Token): -+ - def __init__( -- self, -- token_class, -- # keyjar: KeyJar = None, -- issuer: str = None, -- aud: Optional[list] = None, -- alg: str = "ES256", -- lifetime: int = DEFAULT_TOKEN_LIFETIME, -- upstream_get: Callable = None, -- token_type: str = "Bearer", -- profile: Optional[Union[Message, str]] = JWTAccessToken, -- with_jti: Optional[bool] = False, -- **kwargs -+ self, -+ token_class, -+ # keyjar: KeyJar = None, -+ issuer: str = None, -+ aud: Optional[list] = None, -+ alg: str = "ES256", -+ lifetime: int = DEFAULT_TOKEN_LIFETIME, -+ upstream_get: Callable = None, -+ token_type: str = "Bearer", -+ profile: Optional[Union[Message, str]] = JWTAccessToken, -+ with_jti: Optional[bool] = False, -+ **kwargs - ): - Token.__init__(self, token_class, **kwargs) - self.token_type = token_type -@@ -59,13 +62,13 @@ class JWTToken(Token): - return payload - - def __call__( -- self, -- session_id: Optional[str] = "", -- token_class: Optional[str] = "", -- usage_rules: Optional[dict] = None, -- profile: Optional[Message] = None, -- with_jti: Optional[bool] = None, -- **payload -+ self, -+ session_id: Optional[str] = "", -+ token_class: Optional[str] = "", -+ usage_rules: Optional[dict] = None, -+ profile: Optional[Message] = None, -+ with_jti: Optional[bool] = None, -+ **payload - ) -> str: - """ - Return a token. -@@ -89,8 +92,10 @@ class JWTToken(Token): - lifetime = usage_rules.get("expires_in") - else: - lifetime = self.lifetime -+ _keyjar = self.upstream_get("attribute", "keyjar") -+ logger.info(f"Key owners in the keyjar: {_keyjar.owners()}") - signer = JWT( -- key_jar=self.upstream_get("attribute", "keyjar"), -+ key_jar=_keyjar, - iss=self.issuer, - lifetime=lifetime, - sign_alg=self.alg, -diff --git a/src/idpyoidc/server/xx_metadata.py b/src/idpyoidc/server/xx_metadata.py -new file mode 100644 -index 0000000..a75f852 ---- /dev/null -+++ b/src/idpyoidc/server/xx_metadata.py -@@ -0,0 +1,54 @@ -+from typing import Callable -+from typing import List -+from typing import Optional -+ -+from idpyoidc.message import Message -+ -+from idpyoidc.transform import preferred_to_registered -+ -+ -+class XMetadata(): -+ def __int__(self, upstream_get: Callable): -+ self.upstream_get = upstream_get -+ -+ def get_endpoint_claims(self, entity): -+ _info = {} -+ for endp in entity.server.endpoint.values(): -+ if endp.endpoint_name: -+ _info[endp.endpoint_name] = endp.full_path -+ for arg, claim in [("client_authn_method", "auth_methods"), -+ ("auth_signing_alg_values", "auth_signing_alg_values")]: -+ _val = getattr(endp, arg, None) -+ if _val: -+ # trust_mark_status_endpoint_auth_methods_supported -+ md_param = f"{endp.endpoint_name}_{claim}" -+ _info[md_param] = _val -+ return _info -+ -+ def __call__(self, -+ entity_type: str, -+ metadata_schema: Optional[Message] = None, -+ extra_claims: Optional[List[str]] = None, -+ **kwargs): -+ _claims = self.upstream_get("context").claims -+ entity = self.upstream_get("unit") -+ if not _claims.use: -+ _claims.use = preferred_to_registered(_claims.prefer, supported=entity.supports()) -+ -+ metadata = _claims.use -+ # the claims that can appear in the metadata -+ if metadata_schema: -+ attr = list(metadata_schema.c_param.keys()) -+ else: -+ attr = [] -+ -+ if extra_claims: -+ attr.extend(extra_claims) -+ -+ if attr: -+ metadata = {k:v for k,v in metadata.items() if k in attr} -+ -+ # collect endpoints -+ metadata.update(self.get_endpoint_claims(entity)) -+ # _issuer = getattr(self.server.context, "trust_mark_server", None) -+ return {entity_type: metadata} -diff --git a/src/idpyoidc/storage/abfile.py b/src/idpyoidc/storage/abfile.py -index 6257fe2..d1c088f 100644 ---- a/src/idpyoidc/storage/abfile.py -+++ b/src/idpyoidc/storage/abfile.py -@@ -191,7 +191,7 @@ class AbstractFileSystem(DictType): - else: - return False - else: -- logger.error("Could not access {}".format(fname)) -+ logger.error(f"Not a file '{fname}'") - raise KeyError(item) - - def _read_info(self, fname): -@@ -239,6 +239,14 @@ class AbstractFileSystem(DictType): - else: - self.fmtime[f] = mtime - -+ _keys = self.storage.keys() -+ for f in _keys: -+ fname = os.path.join(self.fdir, f) -+ if os.path.isfile(fname): -+ pass -+ else: -+ del self.storage[f] -+ - def items(self): - """ - Implements the dict.items() method -diff --git a/src/idpyoidc/storage/abfile_no_cache.py b/src/idpyoidc/storage/abfile_no_cache.py -new file mode 100644 -index 0000000..114d45d ---- /dev/null -+++ b/src/idpyoidc/storage/abfile_no_cache.py -@@ -0,0 +1,211 @@ -+import logging -+import os -+import time -+from typing import Optional -+ -+from cryptojwt.utils import importer -+from filelock import FileLock -+ -+from idpyoidc.storage import DictType -+from idpyoidc.util import PassThru -+from idpyoidc.util import QPKey -+ -+logger = logging.getLogger(__name__) -+ -+ -+class AbstractFileSystemNoCache(DictType): -+ """ -+ FileSystem implements a simple file based database. -+ It has a dictionary like interface. -+ Each key maps one-to-one to a file on disc, where the content of the -+ file is the value. -+ ONLY goes one level deep. -+ Not directories in directories. -+ """ -+ -+ def __init__( -+ self, -+ fdir: Optional[str] = "", -+ key_conv: Optional[str] = "", -+ value_conv: Optional[str] = "", -+ read_only: Optional[bool] = False, -+ **kwargs -+ ): -+ """ -+ items = FileSystem( -+ { -+ 'fdir': fdir, -+ 'key_conv':{'to': quote_plus, 'from': unquote_plus}, -+ 'value_conv':{'to': keyjar_to_jwks, 'from': jwks_to_keyjar} -+ }) -+ -+ :param fdir: The root of the directory -+ :param key_conv: Converts to/from the key displayed by this class to -+ users of it to something that can be used as a file name. -+ The value of key_conv is a class that has the methods 'serialize'/'deserialize'. -+ :param value_conv: As with key_conv you can convert/translate -+ the value bound to a key in the database to something that can easily -+ be stored in a file. Like with key_conv the value of this parameter -+ is a class that has the methods 'serialize'/'deserialize'. -+ """ -+ super(AbstractFileSystemNoCache, self).__init__( -+ fdir=fdir, key_conv=key_conv, value_conv=value_conv -+ ) -+ -+ self.fdir = fdir -+ self.read_only = read_only -+ -+ if key_conv: -+ self.key_conv = importer(key_conv)() -+ else: -+ self.key_conv = QPKey() -+ -+ if value_conv: -+ self.value_conv = importer(value_conv)() -+ else: -+ self.value_conv = PassThru() -+ -+ if not os.path.isdir(self.fdir): -+ os.makedirs(self.fdir) -+ -+ def get(self, item, default=None): -+ try: -+ return self[item] -+ except KeyError: -+ return default -+ -+ def __getitem__(self, item): -+ """ -+ Return the value bound to an identifier. -+ -+ :param item: The identifier. -+ :return: -+ """ -+ _file_name = self.key_conv.serialize(item) -+ logger.debug(f'Read from "{_file_name}"') -+ return self._read_info(_file_name) -+ -+ def __setitem__(self, key, value): -+ """ -+ Binds a value to a specific key. If the file that the key maps to -+ does not exist it will be created. The content of the file will be -+ set to the value given. -+ -+ :param key: Identifier -+ :param value: Value that should be bound to the identifier. -+ :return: -+ """ -+ -+ if self.read_only: -+ return -+ -+ if not os.path.isdir(self.fdir): -+ os.makedirs(self.fdir, exist_ok=True) -+ -+ try: -+ _file_name = self.key_conv.serialize(key) -+ except KeyError: -+ _file_name = key -+ -+ fname = os.path.join(self.fdir, _file_name) -+ lock = FileLock(f"{fname}.lock") -+ with lock: -+ with open(fname, "w") as fp: -+ fp.write(self.value_conv.serialize(value)) -+ -+ logger.debug(f'Wrote to "{_file_name}"') -+ -+ def __delitem__(self, key): -+ if self.read_only: -+ return -+ -+ fname = os.path.join(self.fdir, key) -+ if fname.endswith(".lock"): -+ if os.path.isfile(fname): -+ os.unlink(fname) -+ else: -+ if os.path.isfile(fname): -+ lock = FileLock(f"{fname}.lock") -+ with lock: -+ os.unlink(fname) -+ os.unlink(f"{fname}.lock") -+ -+ def _keys(self): -+ """ -+ Implements the dict.keys() method -+ """ -+ keys = [] -+ for f in os.listdir(self.fdir): -+ fname = os.path.join(self.fdir, f) -+ -+ if not os.path.isfile(fname): -+ continue -+ if fname.endswith(".lock"): -+ continue -+ -+ keys.append(f) -+ -+ return keys -+ -+ def keys(self): -+ return [self.key_conv.deserialize(k) for k in self._keys()] -+ -+ def _read_info(self, key): -+ file_name = os.path.join(self.fdir, key) -+ if os.path.isfile(file_name): -+ try: -+ lock = FileLock(f"{file_name}.lock") -+ with lock: -+ info = open(file_name, "r").read().strip() -+ lock.release() -+ return self.value_conv.deserialize(info) -+ except Exception as err: -+ logger.error(err) -+ raise -+ else: -+ _msg = f"No such file: '{file_name}'" -+ logger.error(_msg) -+ return None -+ -+ def items(self): -+ """ -+ Implements the dict.items() method -+ """ -+ for k in self._keys(): -+ v = self._read_info(k) -+ yield self.key_conv.deserialize(k), v -+ -+ def clear(self): -+ """ -+ Completely resets the database. This means that all information in -+ the local cache and on disc will be erased. -+ """ -+ if self.read_only: -+ return -+ -+ if not os.path.isdir(self.fdir): -+ os.makedirs(self.fdir, exist_ok=True) -+ return -+ -+ for f in os.listdir(self.fdir): -+ del self[f] -+ -+ def __contains__(self, item): -+ file_name = os.path.join(self.fdir, self.key_conv.serialize(item)) -+ if os.path.isfile(file_name): -+ return True -+ else: -+ return False -+ -+ def __iter__(self): -+ for k in self._keys(): -+ yield self.key_conv.deserialize(k) -+ -+ def __call__(self, *args, **kwargs): -+ return [self.key_conv.deserialize(k) for k in self._keys()] -+ -+ def __len__(self): -+ if not os.path.isdir(self.fdir): -+ return 0 -+ -+ return len(self._keys()) -diff --git a/src/idpyoidc/storage/listfile.py b/src/idpyoidc/storage/listfile.py -index 77520de..fb515e3 100644 ---- a/src/idpyoidc/storage/listfile.py -+++ b/src/idpyoidc/storage/listfile.py -@@ -111,6 +111,49 @@ class ReadOnlyListFile(object): - else: - return None - -+ def __len__(self): -+ _lst = self._read_info(self.file_name) -+ if _lst is None or _lst == []: -+ return 0 -+ -+ return len(set(_lst)) -+ -+ def _read_info(self, fname): -+ if os.path.isfile(fname): -+ try: -+ lock = FileLock(f"{fname}.lock") -+ with lock: -+ fp = open(fname, "r") -+ info = [x.strip() for x in fp.readlines()] -+ lock.release() -+ return list(set(info)) -+ except Exception as err: -+ logger.error(err) -+ raise -+ else: -+ _msg = f"No such file: '{fname}'" -+ logger.error(_msg) -+ return None -+ -+ def __call__(self): -+ return self._read_info(self.file_name) -+ -+ def list(self): -+ return self._read_info(self.file_name) -+ -+class ReadWriteListFile(object): -+ -+ def __init__(self, file_name): -+ self.file_name = file_name -+ -+ if not os.path.exists(file_name): -+ fp = open(file_name, "x") -+ fp.close() -+ -+ def __contains__(self, item): -+ _lst = self._read_info(self.file_name) -+ return item in _lst -+ - def __len__(self): - _lst = self._read_info(self.file_name) - if _lst is None or _lst == []: -diff --git a/src/idpyoidc/client/claims/transform.py b/src/idpyoidc/transform.py -similarity index 94% -rename from src/idpyoidc/client/claims/transform.py -rename to src/idpyoidc/transform.py -index 1ca40c6..3834006 100644 ---- a/src/idpyoidc/client/claims/transform.py -+++ b/src/idpyoidc/transform.py -@@ -51,10 +51,10 @@ REQUEST2REGISTER = { - - - def supported_to_preferred( -- supported: dict, -- preference: dict, -- base_url: str, -- info: Optional[dict] = None, -+ supported: dict, -+ preference: dict, -+ base_url: str, -+ info: Optional[dict] = None, - ): - if info: # The provider info - for key, val in supported.items(): -@@ -83,7 +83,7 @@ def supported_to_preferred( - preference[key] = [x for x in val if x in _info_val] - else: - pass -- else: -+ elif val: - preference[key] = val - - # special case -> must have a request_uris value -@@ -148,7 +148,7 @@ def _intersection(a, b): - - - def preferred_to_registered( -- prefers: dict, supported: dict, registration_response: Optional[dict] = None -+ prefers: dict, supported: dict, registration_response: Optional[dict] = None - ): - """ - The claims with values that are returned from the OP is what goes unless (!!) -@@ -200,7 +200,7 @@ def preferred_to_registered( - # be a singleton or an array. So just add it as is. - registered[_reg_key] = val - -- logger.debug(f"Entity registered: {registered}") -+ logger.debug(f"preferred2registered: {registered}") - return registered - - -@@ -219,4 +219,10 @@ def create_registration_request(prefers: dict, supported: dict) -> dict: - continue - - _request[key] = array_or_singleton(spec, value) -+ -+ for key, val in prefers.items(): -+ if key not in RegistrationRequest.c_param.keys(): -+ if key not in REGISTER2PREFERRED.values(): -+ _request[key] = val -+ - return _request diff --git a/patch/metadata.patch b/patch/metadata.patch deleted file mode 100644 index 1730fe54..00000000 --- a/patch/metadata.patch +++ /dev/null @@ -1,63 +0,0 @@ -diff --git a/src/idpyoidc/message/__init__.py b/src/idpyoidc/message/__init__.py -index 46d2344..df3e5b3 100644 ---- a/src/idpyoidc/message/__init__.py -+++ b/src/idpyoidc/message/__init__.py -@@ -83,7 +83,8 @@ class Message(MutableMapping): - """ - Creates a string using the application/x-www-form-urlencoded format - -- :doseq: If set to true, key=value pairs separated by '&' are generated for each element of the value sequence for the key. -+ :doseq: If set to true, key=value pairs separated by '&' are generated for each element -+ of the value sequence for the key. - :return: A string of the application/x-www-form-urlencoded format - """ - -@@ -388,7 +389,7 @@ class Message(MutableMapping): - else: - self._dict[skey] = val - else: -- raise DecodeError(ERRTXT % (key, "type != %s" % vtype)) -+ raise DecodeError(ERRTXT % (key, f"type != {vtype}, val:{val}, type:{type(val)}")) - else: - if val is None: - self._dict[skey] = None - -diff --git a/src/idpyoidc/message/oauth2/__init__.py b/src/idpyoidc/message/oauth2/__init__.py -index 788fe8c..95440a9 100644 ---- a/src/idpyoidc/message/oauth2/__init__.py -+++ b/src/idpyoidc/message/oauth2/__init__.py -@@ -560,6 +560,12 @@ class PushedAuthorizationRequest(AuthorizationRequest): - return True - - -+class PushedAuthorizationResponse(ResponseMessage): -+ c_param = ResponseMessage.c_param.copy() -+ c_param.update({"request_uri": SINGLE_REQUIRED_STRING}) -+ -+ -+ - class SecurityEventToken(Message): - c_param = { - "iss": SINGLE_REQUIRED_STRING, - -diff --git a/src/idpyoidc/message/oidc/__init__.py b/src/idpyoidc/message/oidc/__init__.py -index d266224..4cef07a 100644 ---- a/src/idpyoidc/message/oidc/__init__.py -+++ b/src/idpyoidc/message/oidc/__init__.py -@@ -1025,7 +1025,7 @@ class JsonWebToken(Message): - except KeyError: - pass - -- if "iss" in kwargs and "iss" in self: -+ if "iss" in kwargs and kwargs["iss"] and "iss" in self: - if kwargs["iss"] != self["iss"]: - raise ValueError("Wrong issuer") - -@@ -1191,7 +1191,7 @@ def make_openid_request( - :param request_object_signing_alg: Which signing algorithm to use - :param recv: The intended receiver of the request - :param with_jti: Whether a JTI should be included in the JWT. -- :param lifetime: How long the JWT is expect to be live. -+ :param lifetime: How long the JWT is expected to be alive. - :return: JWT encoded OpenID request - """ diff --git a/patch/oauth2.patch b/patch/oauth2.patch deleted file mode 100644 index 444f10a7..00000000 --- a/patch/oauth2.patch +++ /dev/null @@ -1,954 +0,0 @@ - -diff --git a/src/idpyoidc/client/client_auth.py b/src/idpyoidc/client/client_auth.py -index a8830cd..baf03d9 100755 ---- a/src/idpyoidc/client/client_auth.py -+++ b/src/idpyoidc/client/client_auth.py -@@ -10,6 +10,7 @@ from cryptojwt.jws.jws import SIGNER_ALGS - from cryptojwt.jws.utils import alg2keytype - from cryptojwt.utils import importer - -+from idpyoidc.client.request_object import construct_request_parameter - from idpyoidc.defaults import DEF_SIGN_ALG - from idpyoidc.defaults import JWT_BEARER - from idpyoidc.message import Message -@@ -31,6 +32,7 @@ __author__ = "roland hedberg" - - DEFAULT_ACCESS_TOKEN_TYPE = "Bearer" - -+ - class AuthnFailure(Exception): - """Unspecified Authentication failure""" - -@@ -46,7 +48,7 @@ def assertion_jwt(client_id, keys, audience, algorithm, lifetime=600): - - :param client_id: The Client ID - :param keys: Signing keys -- :param audience: Who is the receivers for this assertion -+ :param audience: Who's the receivers for this assertion - :param algorithm: Signing algorithm - :param lifetime: The lifetime of the signed Json Web Token - :return: A Signed Json Web Token -@@ -628,6 +630,12 @@ class PrivateKeyJWT(JWSAuthnMethod): - return keyjar.get_signing_key(alg2keytype(algorithm), "", alg=algorithm) - - -+class RequestParam(ClientAuthnMethod): -+ def construct(self, request, service=None, http_args=None, **kwargs): -+ request_object = construct_request_parameter(service, request, **kwargs) -+ request["request"] = request_object -+ -+ - # Map from client authentication identifiers to corresponding class - CLIENT_AUTHN_METHOD = { - "client_secret_basic": ClientSecretBasic, -@@ -637,6 +645,7 @@ CLIENT_AUTHN_METHOD = { - "client_secret_jwt": ClientSecretJWT, - "private_key_jwt": PrivateKeyJWT, - # "client_notification_authn": ClientNotificationAuthn -+ "request_param": RequestParam - } - - TYPE_METHOD = [(JWT_BEARER, JWSAuthnMethod)] - - -diff --git a/src/idpyoidc/client/entity.py b/src/idpyoidc/client/entity.py -index 197d5d7..2a1b0a6 100644 ---- a/src/idpyoidc/client/entity.py -+++ b/src/idpyoidc/client/entity.py -@@ -103,8 +103,9 @@ class Entity(Unit): # This is a Client. What type is undefined here. - if config is None: - config = {} - -+ # Client ID is set through configuration or at registration - _id = config.get("client_id") -- self.client_id = self.entity_id = entity_id or config.get("entity_id", _id) -+ self.entity_id = entity_id or config.get("entity_id", _id) - - Unit.__init__( - self, -@@ -114,7 +115,7 @@ class Entity(Unit): # This is a Client. What type is undefined here. - httpc_params=httpc_params, - config=config, - key_conf=key_conf, -- client_id=self.client_id, -+ client_id=_id, - ) - - if services: - -diff --git a/src/idpyoidc/client/oauth2/__init__.py b/src/idpyoidc/client/oauth2/__init__.py -index 620608b..20c5c13 100755 ---- a/src/idpyoidc/client/oauth2/__init__.py -+++ b/src/idpyoidc/client/oauth2/__init__.py -@@ -14,6 +14,7 @@ from idpyoidc.client.service import REQUEST_INFO - from idpyoidc.client.service import SUCCESSFUL - from idpyoidc.client.service import Service - from idpyoidc.client.util import do_add_ons -+from idpyoidc.client.util import get_content_type - from idpyoidc.client.util import get_deserialization_method - from idpyoidc.configure import Configuration - from idpyoidc.context import OidcContext -@@ -254,12 +255,13 @@ class Client(Entity): - - if reqresp.status_code in SUCCESSFUL: - logger.debug('response_body_type: "{}"'.format(response_body_type)) -- _deser_method = get_deserialization_method(reqresp) -+ _content_type = get_content_type(reqresp) -+ _deser_method = get_deserialization_method(_content_type) - -- if _deser_method != response_body_type: -+ if _content_type != response_body_type: - logger.warning( - "Not the body type I expected: {} != {}".format( -- _deser_method, response_body_type -+ _content_type, response_body_type - ) - ) - if _deser_method in ["json", "jwt", "urlencoded"]: -@@ -282,7 +284,9 @@ class Client(Entity): - elif 400 <= reqresp.status_code < 500: - logger.error("Error response ({}): {}".format(reqresp.status_code, reqresp.text)) - # expecting an error response -- _deser_method = get_deserialization_method(reqresp) -+ _content_type = get_content_type(reqresp) -+ _deser_method = get_deserialization_method(_content_type) -+ - if not _deser_method: - _deser_method = "json" - - diff --git a/src/idpyoidc/client/oauth2/add_on/dpop.py b/src/idpyoidc/client/oauth2/add_on/dpop.py -index b845050..edd4a0e 100644 ---- a/src/idpyoidc/client/oauth2/add_on/dpop.py -+++ b/src/idpyoidc/client/oauth2/add_on/dpop.py -@@ -1,8 +1,10 @@ -+import base64 - import logging - import uuid - from hashlib import sha256 - from typing import Optional - -+from cryptojwt import as_unicode - from cryptojwt.jwk.jwk import key_from_jwk_dict - from cryptojwt.jws.jws import factory - from cryptojwt.jws.jws import JWS -@@ -16,7 +18,7 @@ from idpyoidc.message import SINGLE_OPTIONAL_STRING - from idpyoidc.message import SINGLE_REQUIRED_INT - from idpyoidc.message import SINGLE_REQUIRED_JSON - from idpyoidc.message import SINGLE_REQUIRED_STRING --from idpyoidc.metadata import get_signing_algs -+from idpyoidc.alg_info import get_signing_algs - from idpyoidc.time_util import utc_time_sans_frac - - logger = logging.getLogger(__name__) -@@ -149,7 +151,7 @@ def dpop_header( - } - - if token: -- header_dict["ath"] = sha256(token.encode("utf8")).hexdigest() -+ header_dict["ath"] = as_unicode(base64.urlsafe_b64encode(sha256(token.encode("utf8")).digest())) - - if nonce: - header_dict["nonce"] = nonce -@@ -168,14 +170,18 @@ def dpop_header( - - def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=None): - """ -- Add the necessary pieces to make pushed authorization happen. -+ Add the necessary pieces to make DPoP happen. - - :param services: A dictionary with all the services the client has access to. -- :param signing_algorithms: Allowed signing algorithms, there is no default algorithms -+ :param dpop_signing_alg_values_supported: Allowed signing algorithms, there is no default algorithms -+ :param with_dpop_header: Which services that should add a DPoP header to a request - """ - -- # Access token request should use DPoP header -- _service = services["accesstoken"] -+ if with_dpop_header is None: -+ with_dpop_header = ["accesstoken", "userinfo"] -+ -+ _service = services[with_dpop_header[0]] -+ # Add to Context - _context = _service.upstream_get("context") - _algs_supported = [ - alg for alg in dpop_signing_alg_values_supported if alg in get_signing_algs() -@@ -186,20 +192,8 @@ def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=No - } - _context.set_preference("dpop_signing_alg_values_supported", _algs_supported) - -- _service.construct_extra_headers.append(dpop_header) -- -- # The same for userinfo requests -- _userinfo_service = services.get("userinfo") -- if _userinfo_service: -- _userinfo_service.construct_extra_headers.append(dpop_header) -- # To be backward compatible -- if with_dpop_header is None: -- with_dpop_header = ["userinfo"] -- -- # Add dpop HTTP header to these -+ # Add dpop HTTP header to requests by these services - for _srv in with_dpop_header: -- if _srv == "accesstoken": -- continue - _service = services.get(_srv) - if _service: - _service.construct_extra_headers.append(dpop_header) - -diff --git a/src/idpyoidc/client/oauth2/add_on/jar.py b/src/idpyoidc/client/oauth2/add_on/jar.py -index a775532..5c98c76 100644 ---- a/src/idpyoidc/client/oauth2/add_on/jar.py -+++ b/src/idpyoidc/client/oauth2/add_on/jar.py -@@ -1,12 +1,9 @@ - import logging - from typing import Optional - --from idpyoidc import claims --from idpyoidc import metadata --from idpyoidc.client.oidc.utils import construct_request_uri --from idpyoidc.client.oidc.utils import request_object_encryption --from idpyoidc.message.oidc import make_openid_request --from idpyoidc.time_util import utc_time_sans_frac -+from idpyoidc import alg_info -+from idpyoidc.client.request_object import construct_request_parameter -+from idpyoidc.client.request_object import construct_request_uri - - logger = logging.getLogger(__name__) - -@@ -35,94 +32,6 @@ def store_request_on_file(service, req, **kwargs): - return _webname - - --def get_request_object_signing_alg(service, **kwargs): -- alg = "" -- for arg in ["request_object_signing_alg", "algorithm"]: -- try: # Trumps everything -- alg = kwargs[arg] -- except KeyError: -- pass -- else: -- break -- -- if not alg: -- _context = service.upstream_get("context") -- alg = _context.add_on["jar"].get("request_object_signing_alg") -- if alg is None: -- alg = "RS256" -- return alg -- -- --def construct_request_parameter(service, req, audience=None, **kwargs): -- """Construct a request parameter""" -- alg = get_request_object_signing_alg(service, **kwargs) -- kwargs["request_object_signing_alg"] = alg -- -- _context = service.upstream_get("context") -- if "keys" not in kwargs and alg and alg != "none": -- kwargs["keys"] = service.upstream_get("attribute", "keyjar") -- -- if alg == "none": -- kwargs["keys"] = [] -- -- # This is the issuer of the JWT, that is me ! -- _issuer = kwargs.get("issuer") -- if _issuer is None: -- kwargs["issuer"] = _context.get_client_id() -- -- if kwargs.get("recv") is None: -- try: -- kwargs["recv"] = _context.provider_info["issuer"] -- except KeyError: -- kwargs["recv"] = _context.issuer -- -- try: -- del kwargs["service"] -- except KeyError: -- pass -- -- _jar_conf = _context.add_on["jar"] -- expires_in = _jar_conf.get("expires_in", DEFAULT_EXPIRES_IN) -- if expires_in: -- req["exp"] = utc_time_sans_frac() + int(expires_in) -- -- if _jar_conf.get("with_jti", False): -- kwargs["with_jti"] = True -- -- _enc_enc = _jar_conf.get("request_object_encryption_enc", "") -- if _enc_enc: -- kwargs["request_object_encryption_enc"] = _enc_enc -- kwargs["request_object_encryption_alg"] = _jar_conf.get("request_object_encryption_alg") -- -- # Filter out only the arguments I want -- _mor_args = { -- k: kwargs[k] -- for k in [ -- "keys", -- "issuer", -- "request_object_signing_alg", -- "recv", -- "with_jti", -- "lifetime", -- ] -- if k in kwargs -- } -- -- if audience: -- _mor_args["aud"] = audience -- -- _req_jwt = make_openid_request(req, **_mor_args) -- -- if "target" not in kwargs: -- kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) -- -- # Should the request be encrypted -- _req_jwte = request_object_encryption( -- _req_jwt, _context, service.upstream_get("attribute", "keyjar"), **kwargs -- ) -- return _req_jwte -- -- - def jar_post_construct(request_args, service, **kwargs): - """ - Modify the request arguments. -@@ -175,14 +84,14 @@ def jar_post_construct(request_args, service, **kwargs): - - - def add_support( -- service, -- request_type: Optional[str] = "request_parameter", -- request_dir: Optional[str] = "", -- request_object_signing_alg: Optional[str] = "RS256", -- expires_in: Optional[int] = DEFAULT_EXPIRES_IN, -- with_jti: Optional[bool] = False, -- request_object_encryption_alg: Optional[str] = "", -- request_object_encryption_enc: Optional[str] = "", -+ service, -+ request_type: Optional[str] = "request_parameter", -+ request_dir: Optional[str] = "", -+ request_object_signing_alg: Optional[str] = "RS256", -+ expires_in: Optional[int] = DEFAULT_EXPIRES_IN, -+ with_jti: Optional[bool] = False, -+ request_object_encryption_alg: Optional[str] = "", -+ request_object_encryption_enc: Optional[str] = "", - ): - """ - JAR support can only be considered if this client can access an authorization service. -@@ -208,8 +117,8 @@ def add_support( - args["request_dir"] = request_dir - - if request_object_encryption_enc and request_object_encryption_alg: -- if request_object_encryption_enc in metadata.get_encryption_encs(): -- if request_object_encryption_alg in metadata.get_encryption_algs(): -+ if request_object_encryption_enc in alg_info.get_encryption_encs(): -+ if request_object_encryption_alg in alg_info.get_encryption_algs(): - args["request_object_encryption_enc"] = request_object_encryption_enc - args["request_object_encryption_alg"] = request_object_encryption_alg - else: -diff --git a/src/idpyoidc/client/oauth2/add_on/par.py b/src/idpyoidc/client/oauth2/add_on/par.py -index afa9405..cfdc349 100644 ---- a/src/idpyoidc/client/oauth2/add_on/par.py -+++ b/src/idpyoidc/client/oauth2/add_on/par.py -@@ -1,8 +1,11 @@ - import logging - -+from cryptojwt import JWT - from cryptojwt.utils import importer - - from idpyoidc.client.client_auth import CLIENT_AUTHN_METHOD -+from idpyoidc.client.oauth2.utils import set_request_object -+from idpyoidc.client.service import Service - from idpyoidc.message import Message - from idpyoidc.message.oauth2 import JWTSecuredAuthorizationRequest - from idpyoidc.server.util import execute -@@ -13,7 +16,7 @@ logger = logging.getLogger(__name__) - HTTP_METHOD = "POST" - - --def push_authorization(request_args, service, **kwargs): -+def push_authorization(request_args: Message, service: Service, **kwargs): - """ - :param request_args: All the request arguments as a AuthorizationRequest instance - :param service: The service to which this post construct method is applied. -@@ -48,19 +51,31 @@ def push_authorization(request_args, service, **kwargs): - ) - _headers["Content-Type"] = "application/x-www-form-urlencoded" - -- # construct the message body -- _body = request_args.to_urlencoded() -+ if isinstance(request_args, Message): -+ _required_params = request_args.to_dict() -+ else: -+ _required_params = request_args -+ -+ _add_request_object = kwargs.get("add_request_object", False) -+ if _add_request_object: -+ _required_params["request"] = set_request_object(service, request_args) -+ -+ _req = service.msg_type(**_required_params) -+ _body = _req.to_urlencoded() - - _http_client = method_args.get("http_client", None) - if not _http_client: - _http_client = service.upstream_get("unit").httpc - - _httpc_params = service.upstream_get("unit").httpc_params -+ _par_endpoint = kwargs.get("pushed_authorization_request_endpoint", None) -+ if not _par_endpoint: -+ _par_endpoint = _context.provider_info["pushed_authorization_request_endpoint"] - - # Send it to the Pushed Authorization Request Endpoint using POST - resp = _http_client( - method=HTTP_METHOD, -- url=_context.provider_info["pushed_authorization_request_endpoint"], -+ url=_par_endpoint, - data=_body, - headers=_headers, - **_httpc_params -@@ -73,10 +88,7 @@ def push_authorization(request_args, service, **kwargs): - _req[param] = request_args.get(param) - request_args = _req - else: -- raise ConnectionError( -- f"Could not connect to " -- f'{_context.provider_info["pushed_authorization_request_endpoint"]}' -- ) -+ raise ConnectionError(f"Could not connect to {_par_endpoint}") - - return request_args - - diff --git a/src/idpyoidc/client/oauth2/authorization.py b/src/idpyoidc/client/oauth2/authorization.py -index 9d85f1f..04ae98d 100644 ---- a/src/idpyoidc/client/oauth2/authorization.py -+++ b/src/idpyoidc/client/oauth2/authorization.py -@@ -31,7 +31,7 @@ class Authorization(Service): - - _supports = { - "response_types_supported": ["code"], -- "response_modes_supported": ["query", "fragment"], -+ "grant_types": None - } - - _callback_path = { - -diff --git a/src/idpyoidc/client/oauth2/pushed_authorization.py b/src/idpyoidc/client/oauth2/pushed_authorization.py -new file mode 100644 -index 0000000..20eb299 ---- /dev/null -+++ b/src/idpyoidc/client/oauth2/pushed_authorization.py -@@ -0,0 +1,89 @@ -+"""The service that talks to the OAuth2 Authorization endpoint.""" -+import logging -+ -+from idpyoidc.client.oauth2.utils import get_state_parameter -+from idpyoidc.client.oauth2.utils import pre_construct_pick_redirect_uri -+from idpyoidc.client.oauth2.utils import set_request_object -+from idpyoidc.client.oauth2.utils import set_state_parameter -+from idpyoidc.client.service import Service -+from idpyoidc.exception import MissingParameter -+from idpyoidc.message import oauth2 -+from idpyoidc.message.oauth2 import ResponseMessage -+from idpyoidc.time_util import time_sans_frac -+ -+LOGGER = logging.getLogger(__name__) -+ -+ -+class PushedAuthorization(Service): -+ """The service that talks to the OAuth2 Pushed Authorization endpoint.""" -+ -+ msg_type = oauth2.PushedAuthorizationRequest -+ response_cls = oauth2.PushedAuthorizationResponse -+ error_msg = ResponseMessage -+ endpoint_name = "pushed_authorization_request_endpoint" -+ service_name = "pushed_authorization" -+ response_body_type = "json" -+ http_method = "POST" -+ -+ _supports = { -+ "response_types_supported": ["code"], -+ "grant_types": None -+ } -+ -+ def __init__(self, upstream_get, conf=None): -+ Service.__init__(self, upstream_get, conf=conf) -+ self.pre_construct.extend([pre_construct_pick_redirect_uri, set_state_parameter]) -+ self.post_construct.append(self.store_auth_request) -+ -+ def add_(self, request_args=None, **kwargs): -+ _add_request_object = kwargs.get("add_request_object", False) -+ if _add_request_object: -+ request_args["request"] = set_request_object(self, request_args) -+ -+ def update_service_context(self, resp, key="", **kwargs): -+ if "expires_in" in resp: -+ resp["__expires_at"] = time_sans_frac() + int(resp["expires_in"]) -+ self.upstream_get("context").cstate.update(key, resp) -+ -+ def store_auth_request(self, request_args=None, **kwargs): -+ """Store the authorization request in the state DB.""" -+ _key = get_state_parameter(request_args, kwargs) -+ self.upstream_get("context").cstate.update(_key, request_args) -+ return request_args -+ -+ def gather_request_args(self, **kwargs): -+ ar_args = Service.gather_request_args(self, **kwargs) -+ -+ if "redirect_uri" not in ar_args: -+ try: -+ ar_args["redirect_uri"] = self.upstream_get("context").get_usage("redirect_uris")[0] -+ except (KeyError, AttributeError): -+ raise MissingParameter("redirect_uri") -+ -+ return ar_args -+ -+ def post_parse_response(self, response, **kwargs): -+ """ -+ Add scope claim to response, from the request, if not present in the -+ response -+ -+ :param response: The response -+ :param kwargs: Extra Keyword arguments -+ :return: A possibly augmented response -+ """ -+ -+ if "scope" not in response: -+ try: -+ _key = kwargs["state"] -+ except KeyError: -+ pass -+ else: -+ if _key: -+ item = self.upstream_get("context").cstate.get_set( -+ _key, message=oauth2.AuthorizationRequest -+ ) -+ try: -+ response["scope"] = item["scope"] -+ except KeyError: -+ pass -+ return response -diff --git a/src/idpyoidc/client/oauth2/registration.py b/src/idpyoidc/client/oauth2/registration.py -index 19da498..ba2ecab 100644 ---- a/src/idpyoidc/client/oauth2/registration.py -+++ b/src/idpyoidc/client/oauth2/registration.py -@@ -4,6 +4,7 @@ from cryptojwt import KeyJar - - from idpyoidc.client.entity import response_types_to_grant_types - from idpyoidc.client.service import Service -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message import oauth2 - from idpyoidc.message.oauth2 import ResponseMessage - -@@ -75,7 +76,7 @@ class Registration(Service): - _keyjar = self.upstream_get("attribute", "keyjar") - if _keyjar: - if _client_id not in _keyjar: -- _keyjar.import_jwks(_keyjar.export_jwks(True, ""), issuer_id=_client_id) -+ _keyjar = store_under_other_id(_keyjar, "", _client_id, True) - _client_secret = _context.get_usage("client_secret") - if _client_secret: - if not _keyjar: - -diff --git a/src/idpyoidc/client/oauth2/stand_alone_client.py b/src/idpyoidc/client/oauth2/stand_alone_client.py -index 8652f56..c456176 100644 ---- a/src/idpyoidc/client/oauth2/stand_alone_client.py -+++ b/src/idpyoidc/client/oauth2/stand_alone_client.py -@@ -18,6 +18,8 @@ from idpyoidc.client.oauth2.utils import pick_redirect_uri - from idpyoidc.exception import MessageException - from idpyoidc.exception import MissingRequiredAttribute - from idpyoidc.exception import NotForMe -+from idpyoidc.key_import import add_kb -+from idpyoidc.key_import import import_jwks_from_file - from idpyoidc.message import Message - from idpyoidc.message.oauth2 import ResponseMessage - from idpyoidc.message.oauth2 import is_error_message -@@ -90,10 +92,10 @@ class StandAloneClient(Client): - elif typ == "file": - for kty, _name in _spec.items(): - if kty == "jwks": -- _kj.import_jwks_from_file(_name, _context.get("issuer")) -+ _kj = import_jwks_from_file(_kj, _name, _context.get("issuer")) - elif kty == "rsa": # PEM file - _kb = keybundle_from_local_file(_name, "der", ["sig"]) -- _kj.add_kb(_context.get("issuer"), _kb) -+ _kj = add_kb(_kj, _context.get("issuer"), _kb) - else: - raise ValueError("Unknown provider JWKS type: {}".format(typ)) - -@@ -746,7 +748,12 @@ def load_registration_response(client, request_args=None): - - :param client: A :py:class:`idpyoidc.client.oidc.Client` instance - """ -- if not client.get_context().get_client_id(): -+ _client_id = getattr(client, "client_id", None) -+ if not _client_id: -+ _context = client.get_context() -+ _client_id = getattr(_context, "client_id", None) -+ -+ if not _client_id: - try: - response = client.do_request("registration", request_args=request_args) - except KeyError: -diff --git a/src/idpyoidc/client/oauth2/utils.py b/src/idpyoidc/client/oauth2/utils.py -index 254e1bd..819ff00 100644 ---- a/src/idpyoidc/client/oauth2/utils.py -+++ b/src/idpyoidc/client/oauth2/utils.py -@@ -2,6 +2,8 @@ import logging - from typing import Optional - from typing import Union - -+from cryptojwt import JWT -+ - from idpyoidc.client.defaults import DEFAULT_RESPONSE_MODE - from idpyoidc.client.service import Service - from idpyoidc.exception import MissingParameter -@@ -99,3 +101,19 @@ def set_state_parameter(request_args=None, **kwargs): - """Assigned a state value.""" - request_args["state"] = get_state_parameter(request_args, kwargs) - return request_args, {"state": request_args["state"]} -+ -+def set_request_object(service, request_args): -+ # construct a signed request object -+ _context = service.upstream_get("context") -+ if _context.keyjar: -+ _jwt = JWT(key_jar=_context.keyjar) -+ else: -+ _jwt = JWT(key_jar=service.upstream_get("attribute", "keyjar")) -+ -+ if isinstance(request_args, Message): -+ _request_object = _jwt.pack(request_args.to_dict()) -+ else: -+ _request_object = _jwt.pack(request_args) -+ -+ # construct the message body -+ return _request_object -\ No newline at end of file - - -diff --git a/src/idpyoidc/server/oauth2/add_on/dpop.py b/src/idpyoidc/server/oauth2/add_on/dpop.py -index 5148cfe..22f37d5 100644 ---- a/src/idpyoidc/server/oauth2/add_on/dpop.py -+++ b/src/idpyoidc/server/oauth2/add_on/dpop.py -@@ -1,3 +1,4 @@ -+import base64 - import logging - from hashlib import sha256 - from typing import Callable -@@ -5,16 +6,17 @@ from typing import Optional - from typing import Union - - from cryptojwt import as_unicode -+from cryptojwt import BadSyntax - from cryptojwt import JWS - from cryptojwt.jwk.jwk import key_from_jwk_dict - from cryptojwt.jws.jws import factory - -+from idpyoidc.alg_info import get_signing_algs - from idpyoidc.message import Message - from idpyoidc.message import SINGLE_OPTIONAL_STRING - from idpyoidc.message import SINGLE_REQUIRED_INT - from idpyoidc.message import SINGLE_REQUIRED_JSON - from idpyoidc.message import SINGLE_REQUIRED_STRING --from idpyoidc.metadata import get_signing_algs - from idpyoidc.server.client_authn import BearerHeader - - logger = logging.getLogger(__name__) -@@ -107,7 +109,14 @@ def token_post_parse_request(request, client_id, context, **kwargs): - if not _http_info: - return request - -- _dpop = DPoPProof().verify_header(_http_info["headers"]["dpop"]) -+ _headers = _http_info['headers'] -+ logger.debug(f"http headers: {_headers}") -+ -+ _dpop_header = _headers.get("dpop", _headers.get("http_dpop", None)) -+ if not _dpop_header: -+ raise ValueError("Missing DPoP header") -+ -+ _dpop = DPoPProof().verify_header(_dpop_header) - - # The signature of the JWS is verified, now for checking the - # content -@@ -126,11 +135,25 @@ def token_post_parse_request(request, client_id, context, **kwargs): - return request - - -+def add_padding(b): -+ # add padding chars -+ m = len(b) % 4 -+ if m == 1: -+ # NOTE: for some reason b64decode raises *TypeError* if the -+ # padding is incorrect. -+ raise BadSyntax(b, "incorrect padding") -+ elif m == 2: -+ b += "==" -+ elif m == 3: -+ b += "=" -+ return b -+ -+ - def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs): - """ - Expect http_info attribute in kwargs. http_info should be a dictionary - containing HTTP information. -- This function is ment for DPoP-protected resources. -+ This function is meant for DPoP-protected resources. - - :param request: - :param client_id: -@@ -143,7 +166,18 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs - if not _http_info: - return request - -- _dpop = DPoPProof().verify_header(_http_info["headers"]["dpop"]) -+ _headers = _http_info.get("headers", "") -+ if _headers: -+ _dpop_header = _headers.get("dpop", "") -+ if not _dpop_header: -+ _dpop_header = _headers.get("http_dpop", "") -+ if not _dpop_header: -+ logger.debug(f"Request Headers: {_headers}") -+ raise ValueError("Expected DPoP header, none found") -+ else: -+ raise ValueError("Expected DPoP header, no headers found") -+ -+ _dpop = DPoPProof().verify_header(_dpop_header) - - # The signature of the JWS is verified, now for checking the - # content -@@ -157,10 +191,19 @@ def userinfo_post_parse_request(request, client_id, context, auth_info, **kwargs - if not _dpop.key: - _dpop.key = key_from_jwk_dict(_dpop["jwk"]) - -- ath = sha256(auth_info["token"].encode("utf8")).hexdigest() -+ _token = auth_info.get("token", None) -+ if _token: -+ # base64.urlsafe_b64encode(sha256(token.encode("utf8")).digest()) -+ ath = as_unicode(base64.urlsafe_b64encode(sha256(_token.encode("utf8")).digest())) - -- if _dpop["ath"] != ath: -- raise ValueError("'ath' in DPoP does not match the token hash") -+ _ath = _dpop.get("ath", None) -+ if _ath is None: -+ raise ValueError("'ath' missing from DPoP") -+ else: -+ _athb = _ath.rstrip("=") -+ _ath = add_padding(_athb) -+ if _ath != ath: -+ raise ValueError("'ath' in DPoP does not match the token hash") - - # Need something I can add as a reference when minting tokens - request["dpop_jkt"] = as_unicode(_dpop.key.thumbprint("SHA-256")) -@@ -184,31 +227,28 @@ def _add_to_context(endpoint, algs_supported): - _context = endpoint.upstream_get("context") - _context.provider_info["dpop_signing_alg_values_supported"] = algs_supported - _context.add_on["dpop"] = {"algs_supported": algs_supported} -- _context.client_authn_methods["dpop"] = DPoPClientAuth -- -+ _context.client_authn_methods["dpop"] = DPoPClientAuth(endpoint.upstream_get) - --def add_support(endpoint: dict, **kwargs): -- # Pick the token endpoint -- _endp = endpoint.get("token", None) -- if _endp: -- _endp.post_parse_request.append(token_post_parse_request) -- _added_to_context = False - -- _algs_supported = kwargs.get("dpop_signing_alg_values_supported") -- if not _algs_supported: -+def add_support(endpoint: dict, dpop_signing_alg_values_supported=None, dpop_endpoints=None, -+ **kwargs): -+ if dpop_signing_alg_values_supported is None: - _algs_supported = ["RS256"] - else: -- _algs_supported = [alg for alg in _algs_supported if alg in get_signing_algs()] -+ # Pick out the ones I support -+ _algs_supported = [alg for alg in dpop_signing_alg_values_supported if -+ alg in get_signing_algs()] -+ -+ _added_to_context = False - -- if _endp: -- _add_to_context(_endp, _algs_supported) -- _added_to_context = True -+ if dpop_endpoints is None: -+ dpop_endpoints = ["userinfo"] - -- for _dpop_endpoint in kwargs.get("dpop_endpoints", ["userinfo"]): -+ for _dpop_endpoint in dpop_endpoints: - _endpoint = endpoint.get(_dpop_endpoint, None) - if _endpoint: - if not _added_to_context: -- _add_to_context(_endp, _algs_supported) -+ _add_to_context(_endpoint, _algs_supported) - _added_to_context = True - - _endpoint.post_parse_request.append(userinfo_post_parse_request) -@@ -220,7 +260,7 @@ def add_support(endpoint: dict, **kwargs): - class DPoPClientAuth(BearerHeader): - tag = "dpop_client_auth" - -- def is_usable(self, request=None, authorization_token=None, http_headers=None): -+ def is_usable(self, request=None, authorization_token=None, http_info=None): - if authorization_token is not None and authorization_token.startswith("DPoP "): - return True - return False -@@ -231,6 +271,7 @@ class DPoPClientAuth(BearerHeader): - authorization_token: Optional[str] = None, - endpoint=None, # Optional[Endpoint] - get_client_id_from_token: Optional[Callable] = None, -+ http_info: Optional[dict] = None, - **kwargs, - ): - # info contains token and client_id -diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py -index 0766af5..223c419 100755 ---- a/src/idpyoidc/server/oauth2/authorization.py -+++ b/src/idpyoidc/server/oauth2/authorization.py -@@ -4,9 +4,9 @@ from typing import List - from typing import Optional - from typing import TypeVar - from typing import Union -+from urllib.parse import parse_qs - from urllib.parse import ParseResult - from urllib.parse import SplitResult --from urllib.parse import parse_qs - from urllib.parse import unquote - from urllib.parse import urlencode - from urllib.parse import urlparse -@@ -18,7 +18,7 @@ from cryptojwt.jws.exception import NoSuitableSigningKeys - from cryptojwt.utils import as_bytes - from cryptojwt.utils import b64e - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.exception import ImproperlyConfigured - from idpyoidc.exception import ParameterError - from idpyoidc.exception import URIError -@@ -48,7 +48,6 @@ from idpyoidc.time_util import utc_time_sans_frac - from idpyoidc.util import importer - from idpyoidc.util import rndstr - -- - ParsedURI = TypeVar('ParsedURI', ParseResult, SplitResult) - - logger = logging.getLogger(__name__) -@@ -192,6 +191,9 @@ def verify_uri( - # Separate the URL from the query string object for the requested redirect URI. - req_redirect_uri_query_obj = parse_qs(req_redirect_uri_obj.query) - req_redirect_uri_without_query_obj = req_redirect_uri_obj._replace(query=None) -+ logger.debug(f"req_redirect_uri_query_obj: {req_redirect_uri_query_obj}") -+ logger.debug(f"req_redirect_uri_without_query_obj: {req_redirect_uri_without_query_obj}") -+ logger.debug(f"client_redirect_uris_obj: {client_redirect_uris_obj}") - - match = any( - req_redirect_uri_without_query_obj == uri_obj -@@ -393,15 +395,16 @@ class Authorization(Endpoint): - _supports = { - "claims_parameter_supported": True, - "request_parameter_supported": True, -- "request_uri_parameter_supported": True, -+ "request_uri_parameter_supported": None, - "response_types_supported": ["code"], - "response_modes_supported": ["query", "fragment", "form_post"], -- "request_object_signing_alg_values_supported": metadata.get_signing_algs(), -- "request_object_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "request_object_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "request_object_encryption_alg_values_supported": [], -+ "request_object_encryption_enc_values_supported": [], - # "grant_types_supported": ["authorization_code", "implicit"], - "code_challenge_methods_supported": ["S256"], - "scopes_supported": [], -+ # "grant_types": [], - } - default_capabilities = { - "client_authn_method": ["request_param", "public"], -@@ -434,8 +437,15 @@ class Authorization(Endpoint): - # If no response_type is registered by the client then we'll use code. - _registered = [{"code"}] - -+ if isinstance(request["response_type"], list): -+ _set = set(request["response_type"]) -+ else: -+ _set = set() -+ _set.add(request["response_type"]) -+ logger.debug(f"Asked for response_type: {_set}") -+ logger.debug(f"Supported response_types: {_registered}") - # Is the asked for response_type among those that are permitted -- return set(request["response_type"]) in _registered -+ return _set in _registered - - def mint_token(self, token_class, grant, session_id, based_on=None, **kwargs): - usage_rules = grant.usage_rules.get(token_class, {}) -@@ -462,15 +472,18 @@ class Authorization(Endpoint): - def _do_request_uri(self, request, client_id, context, **kwargs): - _request_uri = request.get("request_uri") - if _request_uri: -+ logger.debug("Got a 'request_uri") - # Do I do pushed authorization requests ? - _endp = self.upstream_get("endpoint", "pushed_authorization") - if _endp: - # Is it a UUID urn - if _request_uri.startswith("urn:uuid:"): -+ logger.debug("It's a PAR request_uri") - _req = context.par_db.get(_request_uri) - if _req: - # One time usage - del context.par_db[_request_uri] -+ logger.debug(f"Restored request: {_req}") - return _req - else: - raise ValueError("Got a request_uri I can not resolve") -@@ -517,6 +530,7 @@ class Authorization(Endpoint): - request[k] = v - - request[verified_claim_name("request")] = _ver_request -+ logger.debug(f"Fetched request: {request}") - else: - raise ServiceError("Got a %s response", _resp.status) - -@@ -542,7 +556,7 @@ class Authorization(Endpoint): - - _cinfo = context.cdb.get(client_id) - if not _cinfo: -- logger.error("Client ID ({}) not in client database".format(request["client_id"])) -+ logger.error(f"Client ID ({request['client_id']}) not in client database") - return self.authentication_error_response( - request, error="unauthorized_client", error_description="unknown client" - ) -@@ -911,7 +925,7 @@ class Authorization(Endpoint): - - if isinstance(request["response_type"], list): - rtype = set(request["response_type"][:]) -- else: # assume it's a string -+ else: # assume it's a string - rtype = set() - rtype.add(request["response_type"]) - -@@ -1223,10 +1237,12 @@ class AllowedAlgorithms: - _allowed = _cinfo.get(_reg) - if _allowed is None: - _allowed = _pinfo.get(_sup) -+ if _allowed is None: -+ _allowed = [_pinfo.get(_reg)] - - if alg not in _allowed: -- logger.error("Signing alg user: {} not among allowed: {}".format(alg, _allowed)) -- raise ValueError("Not allowed '%s' algorithm used", alg) -+ logger.error(f"Signing alg user: {alg} not among allowed: {_allowed}") -+ raise ValueError(f"Not allowed {alg} algorithm used") - - - def re_authenticate(request, authn) -> bool: -diff --git a/src/idpyoidc/server/oauth2/pushed_authorization.py b/src/idpyoidc/server/oauth2/pushed_authorization.py -index 693b073..52b5616 100644 ---- a/src/idpyoidc/server/oauth2/pushed_authorization.py -+++ b/src/idpyoidc/server/oauth2/pushed_authorization.py -@@ -45,6 +45,6 @@ class PushedAuthorization(Authorization): - self.upstream_get("context").par_db[_urn] = _request - - return { -- "http_response": {"request_uri": _urn, "expires_in": self.ttl}, -+ "response_args": {"request_uri": _urn, "expires_in": self.ttl}, - "return_uri": _request["redirect_uri"], - } - diff --git a/patch/oidc.patch b/patch/oidc.patch deleted file mode 100644 index 34eb24e0..00000000 --- a/patch/oidc.patch +++ /dev/null @@ -1,687 +0,0 @@ - -diff --git a/src/idpyoidc/client/oidc/__init__.py b/src/idpyoidc/client/oidc/__init__.py -index 45cefe2..0ae936f 100755 ---- a/src/idpyoidc/client/oidc/__init__.py -+++ b/src/idpyoidc/client/oidc/__init__.py -@@ -81,18 +81,18 @@ class RP(oauth2.Client): - client_type = "oidc" - - def __init__( -- self, -- keyjar: Optional[KeyJar] = None, -- config: Optional[Union[dict, Configuration]] = None, -- services: Optional[dict] = None, -- httpc: Optional[Callable] = None, -- httpc_params: Optional[dict] = None, -- upstream_get: Optional[Callable] = None, -- key_conf: Optional[dict] = None, -- entity_id: Optional[str] = "", -- verify_ssl: Optional[bool] = True, -- jwks_uri: Optional[str] = "", -- **kwargs -+ self, -+ keyjar: Optional[KeyJar] = None, -+ config: Optional[Union[dict, Configuration]] = None, -+ services: Optional[dict] = None, -+ httpc: Optional[Callable] = None, -+ httpc_params: Optional[dict] = None, -+ upstream_get: Optional[Callable] = None, -+ key_conf: Optional[dict] = None, -+ entity_id: Optional[str] = "", -+ verify_ssl: Optional[bool] = True, -+ jwks_uri: Optional[str] = "", -+ **kwargs - ): - if services: - _srvs = services -diff --git a/src/idpyoidc/client/oidc/access_token.py b/src/idpyoidc/client/oidc/access_token.py -index af629fa..91736d5 100644 ---- a/src/idpyoidc/client/oidc/access_token.py -+++ b/src/idpyoidc/client/oidc/access_token.py -@@ -2,6 +2,7 @@ import logging - from typing import Optional - from typing import Union - -+from idpyoidc.alg_info import get_signing_algs - from idpyoidc.client.client_auth import get_client_authn_methods - from idpyoidc.client.exception import ParameterError - from idpyoidc.client.oauth2 import access_token -@@ -9,7 +10,6 @@ from idpyoidc.client.oidc import IDT2REG - from idpyoidc.message import Message - from idpyoidc.message import oidc - from idpyoidc.message.oidc import verified_claim_name --from idpyoidc.metadata import get_signing_algs - from idpyoidc.time_util import time_sans_frac - - __author__ = "Roland Hedberg" -@@ -34,7 +34,8 @@ class AccessToken(access_token.AccessToken): - access_token.AccessToken.__init__(self, upstream_get, conf=conf) - - def gather_verify_arguments( -- self, response: Optional[Union[dict, Message]] = None, behaviour_args: Optional[dict] = None -+ self, response: Optional[Union[dict, Message]] = None, -+ behaviour_args: Optional[dict] = None - ): - """ - Need to add some information before running verify() -diff --git a/src/idpyoidc/client/oidc/authorization.py b/src/idpyoidc/client/oidc/authorization.py -index 03cde13..e13da13 100644 ---- a/src/idpyoidc/client/oidc/authorization.py -+++ b/src/idpyoidc/client/oidc/authorization.py -@@ -3,22 +3,20 @@ from typing import List - from typing import Optional - from typing import Union - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.client.oauth2 import authorization - from idpyoidc.client.oauth2.utils import pre_construct_pick_redirect_uri - from idpyoidc.client.oidc import IDT2REG --from idpyoidc.client.oidc.utils import construct_request_uri --from idpyoidc.client.oidc.utils import request_object_encryption -+from idpyoidc.client.request_object import construct_request_parameter -+from idpyoidc.client.request_object import construct_request_uri - from idpyoidc.client.service_context import ServiceContext - from idpyoidc.client.util import implicit_response_types - from idpyoidc.exception import MissingRequiredAttribute - from idpyoidc.message import Message - from idpyoidc.message import oauth2 - from idpyoidc.message import oidc --from idpyoidc.message.oidc import make_openid_request - from idpyoidc.message.oidc import verified_claim_name - from idpyoidc.time_util import time_sans_frac --from idpyoidc.time_util import utc_time_sans_frac - from idpyoidc.util import rndstr - - __author__ = "Roland Hedberg" -@@ -32,11 +30,11 @@ class Authorization(authorization.Authorization): - error_msg = oidc.ResponseMessage - - _supports = { -- "request_object_signing_alg_values_supported": metadata.get_signing_algs(), -- "request_object_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "request_object_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "request_object_encryption_alg_values_supported": alg_info.get_encryption_algs(), -+ "request_object_encryption_enc_values_supported": alg_info.get_encryption_encs(), - "response_types_supported": ["code", "id_token", "code id_token"], -- "request_parameter_supported": None, -+ "request_parameter_supported": True, - "request_uri_parameter_supported": None, - "request_uris": None, - "request_parameter": None, -@@ -212,63 +210,6 @@ class Authorization(authorization.Authorization): - fid.close() - return _webname - -- def construct_request_parameter( -- self, req, request_param, audience=None, expires_in=0, **kwargs -- ): -- """Construct a request parameter""" -- alg = self.get_request_object_signing_alg(**kwargs) -- kwargs["request_object_signing_alg"] = alg -- -- _context = self.upstream_get("context") -- if "keys" not in kwargs and alg and alg != "none": -- kwargs["keys"] = self.upstream_get("attribute", "keyjar") -- -- if alg == "none": -- kwargs["keys"] = [] -- -- # This is the issuer of the JWT, that is me ! -- _issuer = kwargs.get("issuer") -- if _issuer is None: -- kwargs["issuer"] = _context.get_client_id() -- -- if kwargs.get("recv") is None: -- try: -- kwargs["recv"] = _context.provider_info["issuer"] -- except KeyError: -- kwargs["recv"] = _context.issuer -- -- try: -- del kwargs["service"] -- except KeyError: -- pass -- -- if expires_in: -- req["exp"] = utc_time_sans_frac() + int(expires_in) -- -- _mor_args = { -- k: kwargs[k] -- for k in [ -- "keys", -- "issuer", -- "request_object_signing_alg", -- "recv", -- "with_jti", -- "lifetime", -- ] -- if k in kwargs -- } -- -- _req_jwt = make_openid_request(req, **_mor_args) -- -- if "target" not in kwargs: -- kwargs["target"] = _context.provider_info.get("issuer", _context.issuer) -- -- # Should the request be encrypted -- _req_jwte = request_object_encryption( -- _req_jwt, _context, self.upstream_get("attribute", "keyjar"), **kwargs -- ) -- return _req_jwte -- - def oidc_post_construct(self, req, **kwargs): - """ - Modify the request arguments. -@@ -303,10 +244,21 @@ class Authorization(authorization.Authorization): - if _request_param == "request_uri": - kwargs["base_path"] = _context.get("base_url") + "/" + "requests" - kwargs["local_dir"] = _context.get_usage("requests_dir", "./requests") -- _req = self.construct_request_parameter(req, _request_param, **kwargs) -+ service = kwargs.get("service") -+ if service: -+ del kwargs["service"] -+ else: -+ service = self -+ -+ _req = construct_request_parameter(service, req, _request_param, **kwargs) - req["request_uri"] = self.store_request_on_file(_req, **kwargs) - elif _request_param == "request": -- _req = self.construct_request_parameter(req, _request_param, **kwargs) -+ service = kwargs.get("service") -+ if service: -+ del kwargs["service"] -+ else: -+ service = self -+ _req = construct_request_parameter(service, req, _request_param, **kwargs) - req["request"] = _req - - if _req: -@@ -319,7 +271,8 @@ class Authorization(authorization.Authorization): - return req - - def gather_verify_arguments( -- self, response: Optional[Union[dict, Message]] = None, behaviour_args: Optional[dict] = None -+ self, response: Optional[Union[dict, Message]] = None, -+ behaviour_args: Optional[dict] = None - ): - """ - Need to add some information before running verify() -@@ -379,12 +332,12 @@ class Authorization(authorization.Authorization): - return "" - - def construct_uris( -- self, -- base_url: str, -- hex: bytes, -- context: ServiceContext, -- targets: Optional[List[str]] = None, -- response_types: Optional[List[str]] = None, -+ self, -+ base_url: str, -+ hex: bytes, -+ context: ServiceContext, -+ targets: Optional[List[str]] = None, -+ response_types: Optional[List[str]] = None, - ): - _callback_uris = context.get_preference("callback_uris", {}) - -diff --git a/src/idpyoidc/client/oidc/registration.py b/src/idpyoidc/client/oidc/registration.py -index 4933905..5c4fef9 100644 ---- a/src/idpyoidc/client/oidc/registration.py -+++ b/src/idpyoidc/client/oidc/registration.py -@@ -4,6 +4,7 @@ from cryptojwt import KeyJar - - from idpyoidc.client.entity import response_types_to_grant_types - from idpyoidc.client.service import Service -+from idpyoidc.key_import import import_jwks - from idpyoidc.message import oidc - from idpyoidc.message.oauth2 import ResponseMessage - -@@ -75,7 +76,7 @@ class Registration(Service): - _keyjar = self.upstream_get("attribute", "keyjar") - if _keyjar: - if _client_id not in _keyjar: -- _keyjar.import_jwks(_keyjar.export_jwks(True, ""), issuer_id=_client_id) -+ _keyjar= import_jwks(_keyjar, _keyjar.export_jwks(True, ""), _client_id) - _client_secret = _context.get_usage("client_secret") - if _client_secret: - if not _keyjar: -@@ -102,7 +103,8 @@ class Registration(Service): - @return: - """ - _context = self.upstream_get("context") -- req_args = _context.claims.create_registration_request() -+ req_args = _context.claims.get_client_metadata(metadata_schema=self.msg_type, -+ supported=_context.supports()) - if "request_args" in self.conf: - req_args.update(self.conf["request_args"]) - -diff --git a/src/idpyoidc/client/oidc/userinfo.py b/src/idpyoidc/client/oidc/userinfo.py -index 05fce76..b92410d 100644 ---- a/src/idpyoidc/client/oidc/userinfo.py -+++ b/src/idpyoidc/client/oidc/userinfo.py -@@ -8,9 +8,9 @@ from idpyoidc.client.service import Service - from idpyoidc.exception import MissingSigningKey - from idpyoidc.message import Message - from idpyoidc.message import oidc --from idpyoidc.metadata import get_encryption_algs --from idpyoidc.metadata import get_encryption_encs --from idpyoidc.metadata import get_signing_algs -+from idpyoidc.alg_info import get_encryption_algs -+from idpyoidc.alg_info import get_encryption_encs -+from idpyoidc.alg_info import get_signing_algs - - logger = logging.getLogger(__name__) - -diff --git a/src/idpyoidc/client/oidc/utils.py b/src/idpyoidc/client/oidc/utils.py -index 2b428fe..e69de29 100644 ---- a/src/idpyoidc/client/oidc/utils.py -+++ b/src/idpyoidc/client/oidc/utils.py -@@ -1,85 +0,0 @@ --import os -- --from cryptojwt.jwe.jwe import JWE --from cryptojwt.jwe.utils import alg2keytype -- --from idpyoidc.exception import MissingRequiredAttribute --from idpyoidc.util import rndstr -- -- --def request_object_encryption(msg, service_context, keyjar, **kwargs): -- """ -- Created an encrypted JSON Web token with *msg* as body. -- -- :param msg: The mesaqg -- :param service_context: -- :param kwargs: -- :return: -- """ -- try: -- encalg = kwargs["request_object_encryption_alg"] -- except KeyError: -- try: -- encalg = service_context.get_usage("request_object_encryption_alg") -- except KeyError: -- return msg -- -- if not encalg: -- return msg -- -- try: -- encenc = kwargs["request_object_encryption_enc"] -- except KeyError: -- try: -- encenc = service_context.get_usage("request_object_encryption_enc") -- except KeyError: -- raise MissingRequiredAttribute("No request_object_encryption_enc specified") -- -- if not encenc: -- raise MissingRequiredAttribute("No request_object_encryption_enc specified") -- -- _jwe = JWE(msg, alg=encalg, enc=encenc) -- _kty = alg2keytype(encalg) -- -- try: -- _kid = kwargs["enc_kid"] -- except KeyError: -- _kid = "" -- -- _target = kwargs.get("target", kwargs.get("recv", None)) -- if _target is None: -- raise MissingRequiredAttribute("No target specified") -- -- if _kid: -- _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target, kid=_kid) -- _jwe["kid"] = _kid -- else: -- _keys = keyjar.get_encrypt_key(_kty, issuer_id=_target) -- -- return _jwe.encrypt(_keys) -- -- --def construct_request_uri(local_dir, base_path, **kwargs): -- """ -- Constructs a special redirect_uri to be used when communicating with -- one OP. Each OP should get their own redirect_uris. -- -- :param local_dir: Local directory in which to place the file -- :param base_path: Base URL to start with -- :param kwargs: -- :return: 2-tuple with (filename, url) -- """ -- _filedir = local_dir -- if not os.path.isdir(_filedir): -- os.makedirs(_filedir) -- _webpath = base_path -- _name = rndstr(10) + ".jwt" -- filename = os.path.join(_filedir, _name) -- while os.path.exists(filename): -- _name = rndstr(10) -- filename = os.path.join(_filedir, _name) -- if _webpath.endswith("/"): -- _webname = f"{_webpath}{_name}" -- else: -- _webname = f"{_webpath}/{_name}" -- return filename, _webname - -diff --git a/src/idpyoidc/server/oidc/authorization.py b/src/idpyoidc/server/oidc/authorization.py -index e6daad3..fc4852e 100644 ---- a/src/idpyoidc/server/oidc/authorization.py -+++ b/src/idpyoidc/server/oidc/authorization.py -@@ -2,7 +2,7 @@ import logging - from typing import Callable - from urllib.parse import urlsplit - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.message import oidc - from idpyoidc.message.oidc import Claims - from idpyoidc.message.oidc import verified_claim_name -@@ -82,11 +82,11 @@ class Authorization(authorization.Authorization): - **{ - "claims_parameter_supported": True, - "encrypt_request_object_supported": False, -- "request_object_signing_alg_values_supported": metadata.get_signing_algs(), -- "request_object_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "request_object_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "request_object_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "request_object_encryption_alg_values_supported": [], -+ "request_object_encryption_enc_values_supported": [], - "request_parameter_supported": True, -- "request_uri_parameter_supported": True, -+ "request_uri_parameter_supported": False, - "require_request_uri_registration": False, - "response_types_supported": ["code", "id_token", "code id_token"], - "response_modes_supported": ["query", "fragment", "form_post"], -diff --git a/src/idpyoidc/server/oidc/backchannel_authentication.py b/src/idpyoidc/server/oidc/backchannel_authentication.py -index b193e22..c45a980 100644 ---- a/src/idpyoidc/server/oidc/backchannel_authentication.py -+++ b/src/idpyoidc/server/oidc/backchannel_authentication.py -@@ -86,10 +86,10 @@ class BackChannelAuthentication(Endpoint): - return set(res) - - def process_request( -- self, -- request: Optional[Union[Message, dict]] = None, -- http_info: Optional[dict] = None, -- **kwargs, -+ self, -+ request: Optional[Union[Message, dict]] = None, -+ http_info: Optional[dict] = None, -+ **kwargs, - ): - try: - request_user = self.do_request_user(request) -@@ -125,6 +125,7 @@ class BackChannelAuthentication(Endpoint): - - - class CIBATokenHelper(AccessTokenHelper): -+ - def _get_session_info(self, request, session_manager): - _path = request["_session_path"] - _grant = session_manager.get(_path) -@@ -137,7 +138,7 @@ class CIBATokenHelper(AccessTokenHelper): - return session_info, _grant - - def post_parse_request( -- self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs -+ self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs - ) -> Union[Message, dict]: - _context = self.endpoint.upstream_get("context") - _mngr = _context.session_manager -@@ -303,10 +304,10 @@ class ClientNotification(Endpoint): - Endpoint.__init__(self, upstream_get, **kwargs) - - def process_request( -- self, -- request: Optional[Union[Message, dict]] = None, -- http_info: Optional[dict] = None, -- **kwargs, -+ self, -+ request: Optional[Union[Message, dict]] = None, -+ http_info: Optional[dict] = None, -+ **kwargs, - ) -> Union[Message, dict]: - return {} - -@@ -316,17 +317,18 @@ class ClientNotificationAuthn(ClientSecretBasic): - - tag = "client_notification_authn" - -- def is_usable(self, request=None, authorization_token=None): -+ def is_usable(self, request=None, authorization_token=None, http_info=None): - if authorization_token is not None and authorization_token.startswith("Bearer "): - return True - return False - - def _verify( -- self, -- authorization_token: Optional[str] = None, -- endpoint=None, # Optional[Endpoint] -- get_client_id_from_token: Optional[Callable] = None, -- **kwargs, -+ self, -+ authorization_token: Optional[str] = None, -+ endpoint=None, # Optional[Endpoint] -+ get_client_id_from_token: Optional[Callable] = None, -+ http_info: Optional[dict] = None, -+ **kwargs, - ): - ttype, token = authorization_token.split(" ", 1) - if ttype != "Bearer": -diff --git a/src/idpyoidc/server/oidc/provider_config.py b/src/idpyoidc/server/oidc/provider_config.py -index 819a699..374ebde 100755 ---- a/src/idpyoidc/server/oidc/provider_config.py -+++ b/src/idpyoidc/server/oidc/provider_config.py -@@ -33,4 +33,14 @@ class ProviderConfiguration(Endpoint): - return request - - def process_request(self, request=None, **kwargs): -- return {"response_args": self.upstream_get("context").provider_info} -+ # return {"response_args": self.upstream_get("context").provider_info} -+ _schema = self.upstream_get("attribute", "metadata_schema") -+ _args = self.upstream_get("context").claims.get_server_metadata(metadata_schema=_schema) -+ # add issuer -+ _args["issuer"] = self.upstream_get("attribute", "entity_id") -+ # add endpoints -+ for name, endpoint in self.upstream_get("unit").endpoint.items(): -+ if endpoint.endpoint_name: -+ _args[endpoint.endpoint_name] = endpoint.full_path -+ -+ return {"response_args": _args} -diff --git a/src/idpyoidc/server/oidc/registration.py b/src/idpyoidc/server/oidc/registration.py -index a363ebe..2ae23cb 100644 ---- a/src/idpyoidc/server/oidc/registration.py -+++ b/src/idpyoidc/server/oidc/registration.py -@@ -12,6 +12,8 @@ from cryptojwt.jws.utils import alg2keytype - from cryptojwt.utils import as_bytes - - from idpyoidc.exception import MessageException -+from idpyoidc.key_import import import_jwks -+from idpyoidc.key_import import import_jwks_as_json - from idpyoidc.message.oauth2 import ResponseMessage - from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB -@@ -143,7 +145,7 @@ class Registration(Endpoint): - # Use my defaults - _my_key = _context.claims.register2preferred.get(claim, claim) - try: -- _val = _context.provider_info[_my_key] -+ _val = _context.claims.get_preference(_my_key) - except KeyError: - return val - -@@ -279,14 +281,23 @@ class Registration(Endpoint): - - t = {"jwks_uri": "", "jwks": None} - -- for item in ["jwks_uri", "jwks"]: -- if item in request: -- t[item] = request[item] -+ _jwks_uri = request.get("jwks_uri") -+ if _jwks_uri: -+ # if it can't load keys because the URL is false it will -+ # just silently fail. Waiting for better times. -+ _keyjar.add_url(issuer_id=client_id, url=_jwks_uri) -+ else: -+ _jwks = request.get("jwks", None) -+ if _jwks: -+ if isinstance(_jwks, str): -+ _keyjar = import_jwks_as_json(_keyjar, _jwks, client_id) -+ else: -+ _keyjar = import_jwks(_keyjar, _jwks, client_id) - -- # if it can't load keys because the URL is false it will -- # just silently fail. Waiting for better times. -- _keyjar.load_keys(client_id, jwks_uri=t["jwks_uri"], jwks=t["jwks"]) -- logger.debug(f"Keys for {client_id}: {_keyjar.key_summary(client_id)}") -+ if client_id in _keyjar: -+ logger.debug(f"Keys for {client_id}: {_keyjar.key_summary(client_id)}") -+ else: -+ logger.debug(f"No keys for {client_id}") - - return _cinfo - -@@ -437,7 +448,13 @@ class Registration(Endpoint): - if not reserved_client_id: - reserved_client_id = _context.cdb.keys() - client_id = cid_generator(reserved=reserved_client_id, **cid_gen_kwargs) -- if "client_id" in request: -+ _entity_id = request.get("client_id", None) -+ if _entity_id: -+ # Already registered -+ _old_id = _context.client_known_as.get(request["client_id"], None) -+ if _old_id: -+ del _context.cdb[_old_id] -+ _context.client_known_as[_entity_id] = client_id - del request["client_id"] - else: - client_id = request.get("client_id") -@@ -456,7 +473,7 @@ class Registration(Endpoint): - if set_secret: - client_secret = self.add_client_secret(_cinfo, client_id, _context) - -- logger.debug("Stored client info in CDB under cid={}".format(client_id)) -+ logger.debug(f"Stored client info in CDB under cid={client_id}") - - _context.cdb[client_id] = _cinfo - _cinfo = self.do_client_registration( -@@ -469,6 +486,12 @@ class Registration(Endpoint): - - args = dict([(k, v) for k, v in _cinfo.items() if k in self.response_cls.c_param]) - -+ # Don't echo keys back -+ try: -+ del args["jwks"] -+ except KeyError: -+ pass -+ - comb_uri(args) - response = self.response_cls(**args) - -@@ -495,7 +518,7 @@ class Registration(Endpoint): - reg_resp = self.client_registration_setup(request, new_id, set_secret, - reserved_client_id) - except Exception as err: -- logger.error("client_registration_setup: %s", request) -+ logger.exception(f"client_registration_setup: {request}") - return ResponseMessage( - error="invalid_configuration_request", error_description="%s" % err - ) -diff --git a/src/idpyoidc/server/oidc/session.py b/src/idpyoidc/server/oidc/session.py -index 03ddfd2..f38e011 100644 ---- a/src/idpyoidc/server/oidc/session.py -+++ b/src/idpyoidc/server/oidc/session.py -@@ -135,7 +135,11 @@ class Session(Endpoint): - try: - alg = cinfo["id_token_signed_response_alg"] - except KeyError: -- alg = _context.provider_info["id_token_signing_alg_values_supported"][0] -+ _algs = _context.provider_info.get("id_token_signing_alg_values_supported") -+ if _algs: -+ alg = _algs[0] -+ else: -+ alg = _context.provider_info.get("id_token_signed_response_alg", "RS256") - - _jws = JWT( - self.upstream_get("attribute", "keyjar"), -diff --git a/src/idpyoidc/server/oidc/token.py b/src/idpyoidc/server/oidc/token.py -index 3436df3..804fa59 100755 ---- a/src/idpyoidc/server/oidc/token.py -+++ b/src/idpyoidc/server/oidc/token.py -@@ -1,6 +1,6 @@ - import logging - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.message import Message - from idpyoidc.message import oidc - from idpyoidc.message.oidc import TokenErrorResponse -@@ -40,7 +40,7 @@ class Token(token.Token): - "client_secret_jwt", - "private_key_jwt", - ], -- "token_endpoint_auth_signing_alg_values_supported": metadata.get_signing_algs(), -+ "token_endpoint_auth_signing_alg_values_supported": alg_info.get_signing_algs(), - "grant_types_supported": list(helper_by_grant_type.keys()), - } - -diff --git a/src/idpyoidc/server/oidc/token_helper/access_token.py b/src/idpyoidc/server/oidc/token_helper/access_token.py -index 2594748..eefc4e2 100755 ---- a/src/idpyoidc/server/oidc/token_helper/access_token.py -+++ b/src/idpyoidc/server/oidc/token_helper/access_token.py -@@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) - - - class AccessTokenHelper(TokenEndpointHelper): -+ - def _get_session_info(self, request, session_manager): - if request["grant_type"] != "authorization_code": - return self.error_cls(error="invalid_request", error_description="Unknown grant_type") -@@ -56,7 +57,7 @@ class AccessTokenHelper(TokenEndpointHelper): - if "grant_types_supported" in _context.cdb[client_id]: - grant_types_supported = _context.cdb[client_id].get("grant_types_supported") - else: -- grant_types_supported = _context.provider_info["grant_types_supported"] -+ grant_types_supported = _context.provider_info.get("grant_types", []) - grant = _session_info["grant"] - - token_type = "Bearer" -@@ -166,7 +167,7 @@ class AccessTokenHelper(TokenEndpointHelper): - return _response - - def post_parse_request( -- self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs -+ self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs - ) -> Union[Message, dict]: - """ - This is where clients come to get their access tokens -diff --git a/src/idpyoidc/server/oidc/userinfo.py b/src/idpyoidc/server/oidc/userinfo.py -index 281b669..6754f37 100755 ---- a/src/idpyoidc/server/oidc/userinfo.py -+++ b/src/idpyoidc/server/oidc/userinfo.py -@@ -9,7 +9,7 @@ from cryptojwt.exception import MissingValue - from cryptojwt.jwt import JWT - from cryptojwt.jwt import utc_time_sans_frac - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.exception import ImproperlyConfigured - from idpyoidc.message import Message - from idpyoidc.message import oidc -@@ -35,9 +35,9 @@ class UserInfo(Endpoint): - _supports = { - "claim_types_supported": ["normal", "aggregated", "distributed"], - "encrypt_userinfo_supported": True, -- "userinfo_signing_alg_values_supported": metadata.get_signing_algs(), -- "userinfo_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "userinfo_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "userinfo_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "userinfo_encryption_alg_values_supported": alg_info.get_encryption_algs(), -+ "userinfo_encryption_enc_values_supported": alg_info.get_encryption_encs(), - } - - def __init__( - diff --git a/patch/test_client_30.patch b/patch/test_client_30.patch deleted file mode 100644 index 0cb6c012..00000000 --- a/patch/test_client_30.patch +++ /dev/null @@ -1,158 +0,0 @@ -diff --git a/tests/test_client_30_rp_handler_oidc.py b/tests/test_client_30_rp_handler_oidc.py -index 3a3d75f..f02aa8d 100644 ---- a/tests/test_client_30_rp_handler_oidc.py -+++ b/tests/test_client_30_rp_handler_oidc.py -@@ -4,12 +4,13 @@ from urllib.parse import parse_qs - from urllib.parse import urlparse - from urllib.parse import urlsplit - --from cryptojwt.key_jar import init_key_jar - import pytest - import responses -+from cryptojwt.key_jar import init_key_jar - - from idpyoidc.client.entity import Entity - from idpyoidc.client.rp_handler import RPHandler -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oidc import AccessTokenResponse - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - from idpyoidc.message.oidc import AuthorizationResponse -@@ -217,6 +218,7 @@ def iss_id(iss): - - - class TestRPHandler(object): -+ - @pytest.fixture(autouse=True) - def rphandler_setup(self): - self.rph = RPHandler( -@@ -270,6 +272,7 @@ class TestRPHandler(object): - 'id_token_signing_alg_values_supported', - 'redirect_uris', - 'request_object_signing_alg_values_supported', -+ 'request_parameter_supported', - 'response_modes_supported', - 'response_types_supported', - 'scopes_supported', -@@ -279,13 +282,13 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - # The key jar should only contain a symmetric key that is the clients - # secret. 2 because one is marked for encryption and the other signing - # usage. - -- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} -+ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} - keys = _keyjar.get_issuer_keys("") - assert len(keys) == 3 - -@@ -329,9 +332,9 @@ class TestRPHandler(object): - assert _context.issuer == _github_id - - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - -- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} -+ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} - keys = _keyjar.get_issuer_keys("") - assert len(keys) == 3 - -@@ -347,7 +350,7 @@ class TestRPHandler(object): - cb = _context.get_preference("callback_uris") - - assert set(cb.keys()) == {"request_uris", "redirect_uris"} -- assert set(cb["redirect_uris"].keys()) == {"query", "fragment"} -+ assert set(cb["redirect_uris"].keys()) == {"query", "fragment", "form_post"} - _hash = _context.iss_hash - - assert cb["redirect_uris"]["query"] == [f"https://example.com/rp/authz_cb/{_hash}"] -@@ -449,7 +452,7 @@ class TestRPHandler(object): - _github_id = iss_id("github") - _context = client.get_context() - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - _nonce = _session["nonce"] - _iss = _session["iss"] -@@ -524,7 +527,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -571,7 +574,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -618,7 +621,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -697,6 +700,7 @@ def test_get_provider_specific_service(): - - - class TestRPHandlerTier2(object): -+ - @pytest.fixture(autouse=True) - def rphandler_setup(self): - self.rph = RPHandler(BASE_URL, CLIENT_CONFIG, keyjar=CLI_KEY) -@@ -712,7 +716,7 @@ class TestRPHandlerTier2(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -818,6 +822,7 @@ class TestRPHandlerTier2(object): - - - class MockResponse: -+ - def __init__(self, status_code, text, headers=None): - self.status_code = status_code - self.text = text -@@ -825,6 +830,7 @@ class MockResponse: - - - class MockOP(object): -+ - def __init__(self, issuer, keyjar=None): - self.keyjar = keyjar - self.issuer = issuer -@@ -913,6 +919,7 @@ def test_rphandler_request(): - - - class TestRPHandlerWithMockOP(object): -+ - @pytest.fixture(autouse=True) - def rphandler_setup(self): - self.issuer = "https://github.com/login/oauth/authorize" -@@ -956,7 +963,7 @@ class TestRPHandlerWithMockOP(object): - ) - _github_id = iss_id("github") - _keyjar = client.get_attribute("keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - with responses.RequestsMock() as rsps: - rsps.add( - "POST", diff --git a/patch/tests.patch b/patch/tests.patch deleted file mode 100644 index d745f2d1..00000000 --- a/patch/tests.patch +++ /dev/null @@ -1,5084 +0,0 @@ - - - - - - -diff --git a/tests/op_config.json b/tests/op_config.json -index 3d0da0b..9ea2da4 100644 ---- a/tests/op_config.json -+++ b/tests/op_config.json -@@ -44,7 +44,7 @@ - } - } - }, -- "capabilities": { -+ "preference": { - "subject_types_supported": [ - "public", - "pairwise" -diff --git a/tests/private/cookie_jwks.json b/tests/private/cookie_jwks.json -index 9d47588..5b507b7 100644 ---- a/tests/private/cookie_jwks.json -+++ b/tests/private/cookie_jwks.json -@@ -1 +1 @@ --{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "4L_0vvQ5QsJvswvh5qCNFyLF4BTSI6xf"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "UsJ7o_W_ND7aoKnbeWEes3MJOECMMY_c"}]} -\ No newline at end of file -+{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "GpKOJkB-QVo3qV2FZMVZFvha-TyJTHeH"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "ugxh7wUNKyolAiXiEWFVL_BVcjaNxvvb"}]} -\ No newline at end of file -diff --git a/tests/private/token_jwks.json b/tests/private/token_jwks.json -index d3e0f07..d171cfa 100644 ---- a/tests/private/token_jwks.json -+++ b/tests/private/token_jwks.json -@@ -1 +1 @@ --{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "vrjoMrmgK8SmJJPc318zTxqG_tvBqF5l"}]} -\ No newline at end of file -+{"keys": [{"kty": "oct", "use": "enc", "kid": "code", "k": "vSHDkLBHhDStkR0NWu8519rmV5zmnm5_"}, {"kty": "oct", "use": "enc", "kid": "refresh", "k": "rJGcHkBJrCCUYp5k62ABrQuUeug_gmL6"}]} -\ No newline at end of file -diff --git a/tests/request123456.jwt b/tests/request123456.jwt -index fed3886..5338a28 100644 ---- a/tests/request123456.jwt -+++ b/tests/request123456.jwt -@@ -1 +1 @@ --eyJhbGciOiJSUzI1NiIsImtpZCI6IlNIRXlZV2N3TlZrMExUZFJPVFp6WjJGVVduZElWWGRhY2sweFdVTTVTRXB3Y1MwM2RWVXhXVTR6UlEifQ.eyJyZXNwb25zZV90eXBlIjogImNvZGUiLCAic3RhdGUiOiAic3RhdGUiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vZXhhbXBsZS5jb20vY2xpL2F1dGh6X2NiIiwgInNjb3BlIjogIm9wZW5pZCIsICJub25jZSI6ICJpcno4SG5ELXFsOVhNYVJYUll1S3BpcEpHM2hiRWZ5akxBYXQwMjNLZEdvIiwgImNsaWVudF9pZCI6ICJjbGllbnRfaWQiLCAiaXNzIjogImNsaWVudF9pZCIsICJpYXQiOiAxNzEwODM2MDQwLCAiYXVkIjogWyJodHRwczovL2V4YW1wbGUuY29tIl19.EDvgPn7QJFm6O4d9QFU9gVZEmAREDIfl1RTiMtec7_ZJ4vGag3dxCyXgz15GbDrQgo6mqCydCe-Mal_4HBlRwMctqhy9NMIGM5PxIKzrqMjsk88jxAoz-WWw3I-pKrJUS4m23mEgLZkGQpB1N3YgO_RhG-7vGCkiJd_8VuomRMd2dX5_Jax3j12T7vhM_TUI9S6XJ5zsLn2ZOPQVXfoprr7HHY6UJjJ65Fp_hoGA3gmfJiHwbxYss8D2X1BNoLmEMze_e6cS-DGe648t2U47E77BvHdzsKi791Y1L3eizkm364gJ371KWbi3avvbSkTi4hEd3OikkyeMQZk6vDiJww -\ No newline at end of file -+eyJhbGciOiJSUzI1NiIsImtpZCI6IlNIRXlZV2N3TlZrMExUZFJPVFp6WjJGVVduZElWWGRhY2sweFdVTTVTRXB3Y1MwM2RWVXhXVTR6UlEifQ.eyJyZXNwb25zZV90eXBlIjogImNvZGUiLCAic3RhdGUiOiAic3RhdGUiLCAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vZXhhbXBsZS5jb20vY2xpL2F1dGh6X2NiIiwgInNjb3BlIjogIm9wZW5pZCIsICJub25jZSI6ICIxcGNuQ3RTSHlrc1JwVzE4N2NWS2p0X2JjTUN0S0dGZzBOZTFyYlhlRXZzIiwgImNsaWVudF9pZCI6ICJjbGllbnRfaWQiLCAiZXhwIjogMTczMDM3NzA5MiwgImlzcyI6ICJjbGllbnRfaWQiLCAiaWF0IjogMTczMDM3MzQ5MiwgImF1ZCI6IFsicmVxdWVzdF91cmkiXSwgImp0aSI6ICI1NzUxZDhmNTVkYzU0OTE1OTI4MjhiMjM0M2YxZTMxZSJ9.k2D_v7jv9cFWIcB6I3HlUPnZg4Tx9FBANR4kPH16fh8cI-c_YFvbCfrXpwUOj1CUY4ZdFqtZnYQD8rQHtjqgXjF79X8H6V6c_7GjQSzHlSAPshFMGm4eXiDKfSCq1xu4YEC-qaub19JjqHpzq6y2Sfz1ayI5qdg_-yJap8HHoPSYzaZ_oPVP7u1TAP24fI15w_leSlgYuXFyzbCWlcWjoHxCJaxYobw3HrAJE4p9h5XL84Rth73xi918CuGw4ngWcF7aQg5dUc1HZPrefU0iVs7Rhi75GmkQzByx7kqIN0T1J-wUod7o69sIqrfWuccam3ndo5E8YlUvwXo0JwKlFQ -\ No newline at end of file -diff --git a/tests/test_04_message.py b/tests/test_04_message.py -index 7fe1878..a3c05b7 100644 ---- a/tests/test_04_message.py -+++ b/tests/test_04_message.py -@@ -14,6 +14,7 @@ from cryptojwt.key_jar import build_keyjar - from idpyoidc.exception import DecodeError - from idpyoidc.exception import MessageException - from idpyoidc.exception import OidcMsgError -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message import OPTIONAL_LIST_OF_MESSAGES - from idpyoidc.message import OPTIONAL_LIST_OF_STRINGS - from idpyoidc.message import OPTIONAL_MESSAGE -@@ -48,13 +49,13 @@ keym = [ - KEYJAR = build_keyjar(keys) - - IKEYJAR = build_keyjar(keys) --IKEYJAR.import_jwks(IKEYJAR.export_jwks(private=True), "issuer") -+IKEYJAR = store_under_other_id(IKEYJAR, "", "issuer", True) - del IKEYJAR[""] - - KEYJARS = {} - for iss in ["A", "B", "C"]: - _kj = build_keyjar(keym) -- _kj.import_jwks(_kj.export_jwks(private=True), iss) -+ _kj = store_under_other_id(_kj, "", iss, True) - del _kj[""] - KEYJARS[iss] = _kj - -diff --git a/tests/test_05_oauth2.py b/tests/test_05_oauth2.py -index fc187db..2cf892c 100644 ---- a/tests/test_05_oauth2.py -+++ b/tests/test_05_oauth2.py -@@ -8,6 +8,7 @@ from cryptojwt.key_jar import build_keyjar - - from idpyoidc import verified_claim_name - from idpyoidc.exception import MissingRequiredAttribute -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message import DecodeError - from idpyoidc.message import json_deserializer - from idpyoidc.message import json_serializer -@@ -44,10 +45,10 @@ keym = [ - ] - - KEYJAR = build_keyjar(keys) --KEYJAR.import_jwks(KEYJAR.export_jwks(private=True), "issuer") -+KEYJAR = store_under_other_id(KEYJAR,"", "issuer", True) - - IKEYJAR = build_keyjar(keys) --IKEYJAR.import_jwks(IKEYJAR.export_jwks(private=True), "issuer") -+IKEYJAR = store_under_other_id(IKEYJAR, "", "issuer", True) - del IKEYJAR[""] - - -diff --git a/tests/test_07_session.py b/tests/test_07_session.py -index 94cbebd..b380635 100644 ---- a/tests/test_07_session.py -+++ b/tests/test_07_session.py -@@ -10,6 +10,8 @@ from cryptojwt.key_jar import init_key_jar - - from idpyoidc.exception import MessageException - from idpyoidc.exception import NotForMe -+from idpyoidc.key_import import import_jwks_as_json -+from idpyoidc.key_import import import_jwks_from_file - from idpyoidc.message.oidc import Claims - from idpyoidc.message.oidc import ClaimsRequest - from idpyoidc.message.oidc import IdToken -@@ -64,8 +66,8 @@ ISS_KEY = init_key_jar( - issuer_id=ISS, - ) - --ISS_KEY.import_jwks_as_json(open(full_path("pub_client.jwks")).read(), CLIENT_ID) --CLI_KEY.import_jwks_as_json(open(full_path("pub_iss.jwks")).read(), ISS) -+ISS_KEY = import_jwks_from_file(ISS_KEY, full_path("pub_client.jwks"), CLIENT_ID) -+CLI_KEY = import_jwks_from_file(CLI_KEY, full_path("pub_iss.jwks"), ISS) - - - class TestEndSessionResponse(object): -diff --git a/tests/test_08_transform.py b/tests/test_08_transform.py -index 71c83d9..33a2e14 100644 ---- a/tests/test_08_transform.py -+++ b/tests/test_08_transform.py -@@ -4,12 +4,12 @@ import pytest - from cryptojwt.utils import importer - - from idpyoidc.client.claims.oidc import Claims as OIDC_Claims --from idpyoidc.client.claims.transform import create_registration_request --from idpyoidc.client.claims.transform import preferred_to_registered --from idpyoidc.client.claims.transform import supported_to_preferred - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - from idpyoidc.message.oidc import ProviderConfigurationResponse - from idpyoidc.message.oidc import RegistrationRequest -+from idpyoidc.transform import create_registration_request -+from idpyoidc.transform import preferred_to_registered -+from idpyoidc.transform import supported_to_preferred - - - class TestTransform: -@@ -42,117 +42,114 @@ class TestTransform: - - def test_supported(self): - # These are all the available configuration parameters -- assert set(self.supported.keys()) == { -- "acr_values_supported", -- "application_type", -- "backchannel_logout_session_required", -- "backchannel_logout_supported", -- "backchannel_logout_uri", -- "callback_uris", -- "client_id", -- "client_name", -- "client_secret", -- "client_uri", -- "contacts", -- "default_max_age", -- "encrypt_id_token_supported", -- "encrypt_request_object_supported", -- "encrypt_userinfo_supported", -- "frontchannel_logout_session_required", -- "frontchannel_logout_supported", -- "frontchannel_logout_uri", -- "id_token_encryption_alg_values_supported", -- "id_token_encryption_enc_values_supported", -- "id_token_signing_alg_values_supported", -- "initiate_login_uri", -- "jwks", -- "jwks_uri", -- "logo_uri", -- "policy_uri", -- "post_logout_redirect_uris", -- "redirect_uris", -- "request_object_encryption_alg_values_supported", -- "request_object_encryption_enc_values_supported", -- "request_object_signing_alg_values_supported", -- "request_parameter", -- "request_parameter_supported", -- "request_uri_parameter_supported", -- "request_uris", -- "requests_dir", -- "require_auth_time", -- "response_modes_supported", -- "response_types_supported", -- "scopes_supported", -- "sector_identifier_uri", -- "subject_types_supported", -- # 'token_endpoint_auth_method', -- "token_endpoint_auth_methods_supported", -- "token_endpoint_auth_signing_alg_values_supported", -- "tos_uri", -- "userinfo_encryption_alg_values_supported", -- "userinfo_encryption_enc_values_supported", -- "userinfo_signing_alg_values_supported", -- } -+ assert set(self.supported.keys()) == {'acr_values_supported', -+ 'application_type', -+ 'backchannel_logout_session_required', -+ 'backchannel_logout_supported', -+ 'backchannel_logout_uri', -+ 'callback_uris', -+ 'client_id', -+ 'client_name', -+ 'client_secret', -+ 'client_uri', -+ 'code_challenge_methods_supported', -+ 'contacts', -+ 'default_max_age', -+ 'encrypt_id_token_supported', -+ 'encrypt_request_object_supported', -+ 'encrypt_userinfo_supported', -+ 'frontchannel_logout_session_required', -+ 'frontchannel_logout_supported', -+ 'frontchannel_logout_uri', -+ 'id_token_encryption_alg_values_supported', -+ 'id_token_encryption_enc_values_supported', -+ 'id_token_signing_alg_values_supported', -+ 'initiate_login_uri', -+ 'jwks', -+ 'jwks_uri', -+ 'logo_uri', -+ 'policy_uri', -+ 'post_logout_redirect_uris', -+ 'redirect_uris', -+ 'request_object_encryption_alg_values_supported', -+ 'request_object_encryption_enc_values_supported', -+ 'request_object_signing_alg_values_supported', -+ 'request_parameter', -+ 'request_parameter_supported', -+ 'request_uri_parameter_supported', -+ 'request_uris', -+ 'requests_dir', -+ 'require_auth_time', -+ 'response_modes_supported', -+ 'response_types_supported', -+ 'scopes_supported', -+ 'sector_identifier_uri', -+ 'subject_types_supported', -+ 'token_endpoint_auth_methods_supported', -+ 'token_endpoint_auth_signing_alg_values_supported', -+ 'tos_uri', -+ 'userinfo_encryption_alg_values_supported', -+ 'userinfo_encryption_enc_values_supported', -+ 'userinfo_signing_alg_values_supported'} - - def test_oidc_setup(self): - # This is OP specified stuff - assert set(ProviderConfigurationResponse.c_param.keys()).difference( - set(self.supported) - ) == { -- "authorization_endpoint", -- "check_session_iframe", -- "claim_types_supported", -- "claims_locales_supported", -- "claims_parameter_supported", -- "claims_supported", -- "display_values_supported", -- "end_session_endpoint", -- "error", -- "error_description", -- "error_uri", -- "grant_types_supported", -- "issuer", -- "op_policy_uri", -- "op_tos_uri", -- "registration_endpoint", -- "require_request_uri_registration", -- "service_documentation", -- "token_endpoint", -- "ui_locales_supported", -- "userinfo_endpoint", -- "code_challenge_methods_supported", -- } -+ "authorization_endpoint", -+ "check_session_iframe", -+ "claim_types_supported", -+ "claims_locales_supported", -+ "claims_parameter_supported", -+ "claims_supported", -+ "display_values_supported", -+ "end_session_endpoint", -+ "error", -+ "error_description", -+ "error_uri", -+ "grant_types_supported", -+ "issuer", -+ "op_policy_uri", -+ "op_tos_uri", -+ "registration_endpoint", -+ "require_request_uri_registration", -+ "service_documentation", -+ "token_endpoint", -+ "ui_locales_supported", -+ "userinfo_endpoint", -+ } - - # parameters that are not mapped against what the OP's provider info says - assert set(self.supported).difference( - set(ProviderConfigurationResponse.c_param.keys()) - ) == { -- "application_type", -- "backchannel_logout_uri", -- "callback_uris", -- "client_id", -- "client_name", -- "client_secret", -- "client_uri", -- "contacts", -- "default_max_age", -- "encrypt_id_token_supported", -- "encrypt_request_object_supported", -- "encrypt_userinfo_supported", -- "frontchannel_logout_uri", -- "initiate_login_uri", -- "jwks", -- "logo_uri", -- "policy_uri", -- "post_logout_redirect_uris", -- "redirect_uris", -- "request_parameter", -- "request_uris", -- "requests_dir", -- "require_auth_time", -- "sector_identifier_uri", -- "tos_uri", -- } -+ "application_type", -+ "backchannel_logout_uri", -+ "callback_uris", -+ "client_id", -+ "client_name", -+ "client_secret", -+ "client_uri", -+ "contacts", -+ "default_max_age", -+ "encrypt_id_token_supported", -+ "encrypt_request_object_supported", -+ "encrypt_userinfo_supported", -+ "frontchannel_logout_uri", -+ "initiate_login_uri", -+ "jwks", -+ "logo_uri", -+ "policy_uri", -+ "post_logout_redirect_uris", -+ "redirect_uris", -+ "request_parameter", -+ "request_uris", -+ "requests_dir", -+ "require_auth_time", -+ "sector_identifier_uri", -+ "tos_uri", -+ } - - claims = OIDC_Claims() - # No input from the IDP so info is absent -@@ -173,6 +170,7 @@ class TestTransform: - "request_object_encryption_alg_values_supported", - "request_object_encryption_enc_values_supported", - "request_object_signing_alg_values_supported", -+ "request_parameter_supported", - "response_modes_supported", - "response_types_supported", - "scopes_supported", -@@ -245,27 +243,24 @@ class TestTransform: - ) - - # These are the claims that has default values -- assert set(claims.prefer.keys()) == { -- "application_type", -- "default_max_age", -- "encrypt_request_object_supported", -- "encrypt_userinfo_supported", -- "id_token_encryption_alg_values_supported", -- "id_token_encryption_enc_values_supported", -- "id_token_signing_alg_values_supported", -- "request_object_encryption_alg_values_supported", -- "request_object_encryption_enc_values_supported", -- "request_object_signing_alg_values_supported", -- "response_modes_supported", -- "response_types_supported", -- "scopes_supported", -- "subject_types_supported", -- "token_endpoint_auth_methods_supported", -- "token_endpoint_auth_signing_alg_values_supported", -- "userinfo_encryption_alg_values_supported", -- "userinfo_encryption_enc_values_supported", -- "userinfo_signing_alg_values_supported", -- } -+ assert set(claims.prefer.keys()) == {'application_type', -+ 'default_max_age', -+ 'id_token_encryption_alg_values_supported', -+ 'id_token_encryption_enc_values_supported', -+ 'id_token_signing_alg_values_supported', -+ 'request_object_encryption_alg_values_supported', -+ 'request_object_encryption_enc_values_supported', -+ 'request_object_signing_alg_values_supported', -+ 'request_parameter_supported', -+ 'response_modes_supported', -+ 'response_types_supported', -+ 'scopes_supported', -+ 'subject_types_supported', -+ 'token_endpoint_auth_methods_supported', -+ 'token_endpoint_auth_signing_alg_values_supported', -+ 'userinfo_encryption_alg_values_supported', -+ 'userinfo_encryption_enc_values_supported', -+ 'userinfo_signing_alg_values_supported'} - - # least common denominator - # The RP supports less than the OP -@@ -362,10 +357,13 @@ class TestTransform2: - "client_name", - "contacts", - "default_max_age", -+ 'encrypt_request_object_supported', -+ 'encrypt_userinfo_supported', - "id_token_signed_response_alg", - "logo_uri", - "redirect_uris", - "request_object_signing_alg", -+ 'request_parameter_supported', - "response_types", - "response_modes", # non-standard - "subject_type", -@@ -402,29 +400,28 @@ class TestTransform2: - registration_response=registration_response, - ) - -- assert set(to_use.keys()) == { -- "application_type", -- "client_name", -- "contacts", -- "default_max_age", -- "encrypt_request_object_supported", -- "encrypt_userinfo_supported", -- "id_token_signed_response_alg", -- "jwks_uri", -- "logo_uri", -- "redirect_uris", -- "request_object_signing_alg", -- "request_uris", -- "response_types", -- "response_modes", # non-standard -- "scope", -- "sector_identifier_uri", -- "subject_type", -- "token_endpoint_auth_method", -- "token_endpoint_auth_signing_alg", -- "userinfo_encrypted_response_alg", -- "userinfo_encrypted_response_enc", -- "userinfo_signed_response_alg", -- } -+ assert set(to_use.keys()) == {'application_type', -+ 'client_name', -+ 'contacts', -+ 'default_max_age', -+ 'id_token_signed_response_alg', -+ 'jwks_uri', -+ 'logo_uri', -+ 'redirect_uris', -+ 'request_object_signing_alg', -+ 'request_uris', -+ 'encrypt_userinfo_supported', -+ 'request_parameter_supported', -+ 'encrypt_request_object_supported', -+ 'response_modes', -+ 'response_types', -+ 'scope', -+ 'sector_identifier_uri', -+ 'subject_type', -+ 'token_endpoint_auth_method', -+ 'token_endpoint_auth_signing_alg', -+ 'userinfo_encrypted_response_alg', -+ 'userinfo_encrypted_response_enc', -+ 'userinfo_signed_response_alg'} - - assert to_use["subject_type"] == "pairwise" -diff --git a/tests/test_09_work_condition.py b/tests/test_09_work_condition.py -index 957d857..34dd006 100644 ---- a/tests/test_09_work_condition.py -+++ b/tests/test_09_work_condition.py -@@ -4,9 +4,10 @@ import pytest as pytest - from cryptojwt.utils import importer - - from idpyoidc.client.claims.oidc import Claims --from idpyoidc.client.claims.transform import create_registration_request --from idpyoidc.client.claims.transform import preferred_to_registered --from idpyoidc.client.claims.transform import supported_to_preferred -+from idpyoidc.message.oidc import RegistrationRequest -+from idpyoidc.transform import create_registration_request -+from idpyoidc.transform import preferred_to_registered -+from idpyoidc.transform import supported_to_preferred - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - - KEYSPEC = [ -@@ -168,14 +169,14 @@ class TestWorkEnvironment: - "acr_values_supported": ["mfa"], - } - -- pref = self.claims.prefer = supported_to_preferred( -+ self.claims.prefer = supported_to_preferred( - supported=self.supported, - preference=self.claims.prefer, - base_url="https://example.com", - info=provider_info_response, - ) - -- registration_request = create_registration_request(self.claims.prefer, self.supported) -+ registration_request = self.claims.get_client_metadata(metadata_schema=RegistrationRequest) - - assert set(registration_request.keys()) == { - "application_type", -@@ -230,8 +231,8 @@ class TestWorkEnvironment: - "client_secret", - "contacts", - "default_max_age", -- "encrypt_request_object_supported", -- "encrypt_userinfo_supported", -+ 'encrypt_userinfo_supported', -+ 'encrypt_request_object_supported', - "id_token_signed_response_alg", - "jwks", - "jwks_uri", -@@ -239,6 +240,7 @@ class TestWorkEnvironment: - "redirect_uris", - "request_object_signing_alg", - "request_uris", -+ 'request_parameter_supported', - "response_modes", - "response_types", - "scope", -@@ -313,7 +315,7 @@ class TestWorkEnvironment: - info=provider_info_response, - ) - -- registration_request = create_registration_request(self.claims.prefer, self.supported) -+ registration_request = self.claims.get_client_metadata(metadata_schema=RegistrationRequest) - - assert set(registration_request.keys()) == { - "application_type", -@@ -376,6 +378,7 @@ class TestWorkEnvironment: - "logo_uri", - "redirect_uris", - "request_object_signing_alg", -+ 'request_parameter_supported', - "request_uris", - "response_modes", - "response_types", -diff --git a/tests/test_11_impexp.py b/tests/test_11_impexp.py -index 3f525b1..14e1e4e 100644 ---- a/tests/test_11_impexp.py -+++ b/tests/test_11_impexp.py -@@ -94,3 +94,27 @@ def test_flush(): - assert len(b.bundles) == 2 - for kb in b.bundles: - assert isinstance(kb, KeyBundle) -+ -+ -+def test_dict(): -+ b = ImpExpTest() -+ b.string = "foo" -+ b.list = ["a", "b", "c"] -+ b.dict = {"a": 1, "b": 2} -+ b.message = { -+ "scope": "openid", -+ "redirect_uri": "https://example.com/cb", -+ "response_type": "code", -+ "client_id": "abcdefg", -+ } -+ -+ dump = b.dump() -+ -+ b.flush() -+ -+ b.load(dump) -+ -+ assert b.string == "foo" -+ assert b.list == ["a", "b", "c"] -+ assert b.dict == {"a": 1, "b": 2} -+ assert isinstance(b.message, AuthorizationRequest) -diff --git a/tests/test_14_read_only_list_file.py b/tests/test_14_read_only_list_file.py -index 2abdf9e..fd30b9d 100644 ---- a/tests/test_14_read_only_list_file.py -+++ b/tests/test_14_read_only_list_file.py -@@ -24,6 +24,4 @@ def test_read_only_list_file(): - fp.write(line + '\n') - - # sleep(2) -- # assert _read_only.is_changed(FILE_NAME) is True - assert set(_read_only) == {"one", "two", "three"} -- assert _read_only[-1] == "three" -\ No newline at end of file -diff --git a/tests/test_20_config.py b/tests/test_20_config.py -index ad737ec..3f6b27a 100644 ---- a/tests/test_20_config.py -+++ b/tests/test_20_config.py -@@ -172,14 +172,12 @@ def test_init_crypto_keys(): - "keys": { - "private_path": "private/cookie_jwks.json", - "key_defs": [ -- {"type": "OCT", "use": ["enc"], "kid": "enc"}, -- {"type": "OCT", "use": ["sig"], "kid": "sig"}, -+ {"type": "OCT", "use": ["enc"], "kid": "key", "bytes": 32}, - ], - "read_only": False, - } - } - _res = init_encrypter(_conf) - assert _res["conf"]["class"] == DEFAULT_CRYPTO -- assert set(_res["conf"]["kwargs"].keys()) == {"password", "salt"} -- assert "password" in _res["conf"]["kwargs"] -- assert "salt" in _res["conf"]["kwargs"] -+ assert set(_res["conf"]["kwargs"].keys()) == {"key", "salt"} -+ assert len(_res["conf"]["kwargs"]["salt"]) == 16 -diff --git a/tests/test_21_abfile_no_cache.py b/tests/test_21_abfile_no_cache.py -new file mode 100644 -index 0000000..cfe7e86 ---- /dev/null -+++ b/tests/test_21_abfile_no_cache.py -@@ -0,0 +1,116 @@ -+import os -+import shutil -+ -+import pytest -+ -+from idpyoidc.impexp import ImpExp -+from idpyoidc.storage.abfile_no_cache import AbstractFileSystemNoCache -+ -+BASEDIR = os.path.abspath(os.path.dirname(__file__)) -+ -+ -+def full_path(local_file): -+ return os.path.join(BASEDIR, local_file) -+ -+ -+CLIENT_1 = { -+ "client_secret": "hemligtkodord", -+ "redirect_uris": [["https://example.com/cb", ""]], -+ "client_salt": "salted", -+ "token_endpoint_auth_method": "client_secret_post", -+ "response_types": ["code", "token"], -+} -+ -+CLIENT_2 = { -+ "client_secret": "spraket", -+ "redirect_uris": [["https://app1.example.net/foo", ""], ["https://app2.example.net/bar", ""]], -+ "response_types": ["code"], -+} -+ -+ -+class ImpExpTest(ImpExp): -+ parameter = { -+ "string": "", -+ "list": [], -+ "dict": "DICT_TYPE", -+ } -+ -+ -+class TestAFS(object): -+ @pytest.fixture(autouse=True) -+ def setup(self): -+ filename = full_path("afs") -+ if os.path.isdir(filename): -+ shutil.rmtree(filename) -+ -+ def test_create_cdb(self): -+ abf = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") -+ -+ # add a client -+ -+ abf["client_1"] = CLIENT_1 -+ -+ assert list(abf.keys()) == ["client_1"] -+ -+ # add another one -+ -+ abf["client_2"] = CLIENT_2 -+ -+ assert set(abf.keys()) == {"client_1", "client_2"} -+ -+ def test_read_cdb(self): -+ abf = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") -+ # add a client -+ abf["client_1"] = CLIENT_1 -+ # add another one -+ abf["client_2"] = CLIENT_2 -+ -+ afs_2 = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") -+ assert set(afs_2.keys()) == {"client_1", "client_2"} -+ -+ def test_dump_load_afs(self): -+ b = ImpExpTest() -+ b.string = "foo" -+ b.list = ["a", "b", "c"] -+ b.dict = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") -+ -+ # add a client -+ b.dict["client_1"] = CLIENT_1 -+ # add another one -+ b.dict["client_2"] = CLIENT_2 -+ -+ dump = b.dump() -+ -+ b_copy = ImpExpTest().load(dump) -+ assert b_copy -+ assert isinstance(b_copy.dict, AbstractFileSystemNoCache) -+ assert set(b_copy.dict.keys()) == {"client_1", "client_2"} -+ -+ def test_dump_load_dict(self): -+ b = ImpExpTest() -+ b.string = "foo" -+ b.list = ["a", "b", "c"] -+ b.dict = {"a": 1, "b": 2, "c": 3} -+ -+ dump = b.dump() -+ -+ b_copy = ImpExpTest().load(dump) -+ assert b_copy -+ assert isinstance(b_copy.dict, dict) -+ -+ def test_get(self): -+ abf = AbstractFileSystemNoCache(fdir=full_path("afs"), value_conv="idpyoidc.util.JSON") -+ # add a client -+ abf["client_1"] = CLIENT_1 -+ # add another one -+ abf["client_2"] = CLIENT_2 -+ -+ val = abf["client_2"] -+ assert val == CLIENT_2 -+ -+ del abf["client_2"] -+ -+ assert set(abf.keys()) == {"client_1"} -+ -+ abf.clear() -+ assert set(abf.keys()) == set() -diff --git a/tests/test_client_02_entity.py b/tests/test_client_02_entity.py -index 369ef7f..0598146 100644 ---- a/tests/test_client_02_entity.py -+++ b/tests/test_client_02_entity.py -@@ -39,7 +39,7 @@ class TestEntity: - assert _srv is None - - def test_get_client_id(self): -- assert self.entity.get_service_context().get_preference("client_id") == "Number5" -+ assert self.entity.client_id == "Number5" - assert self.entity.get_attribute("client_id") == "Number5" - - def test_get_service_by_endpoint_name(self): -diff --git a/tests/test_client_02b_entity_metadata.py b/tests/test_client_02b_entity_metadata.py -index 8f122a5..16002e4 100644 ---- a/tests/test_client_02b_entity_metadata.py -+++ b/tests/test_client_02b_entity_metadata.py -@@ -74,6 +74,7 @@ def test_create_client(): - "redirect_uris", - "request_object_signing_alg_values_supported", - "request_parameter", -+ "request_parameter_supported", - "response_modes_supported", - "response_types_supported", - "scopes_supported", -@@ -95,7 +96,7 @@ def test_create_client(): - - _conf_args = list(_context.collect_usage().keys()) - assert _conf_args -- assert len(_conf_args) == 23 -+ assert len(_conf_args) == 24 - rr = set(RegistrationRequest.c_param.keys()) - # The ones that are not defined and will therefore not appear in a registration request - d = rr.difference(set(_conf_args)) -@@ -146,3 +147,28 @@ def test_create_client_jwks_uri(): - client_config["jwks_uri"] = "https://rp.example.com/jwks_uri.json" - client = Entity(config=client_config) - assert client.get_service_context().get_preference("jwks_uri") -+ -+ -+def test_metadata(): -+ client = Entity(config=CLIENT_CONFIG, client_type="oidc") -+ # With entity type -+ metadata = client.context.claims.get_client_metadata("openid_relying_party", -+ metadata_schema=RegistrationRequest) -+ assert set(metadata.keys()) == {"openid_relying_party"} -+ # Without entity type, no endpoints. Typical client -+ metadata = client.context.claims.get_client_metadata(metadata_schema=RegistrationRequest) -+ assert set(metadata.keys()) == {'application_type', -+ 'backchannel_logout_session_required', -+ 'backchannel_logout_uri', -+ 'contacts', -+ 'default_max_age', -+ 'grant_types', -+ 'id_token_signed_response_alg', -+ 'redirect_uris', -+ 'request_object_signing_alg', -+ 'response_modes', -+ 'response_types', -+ 'subject_type', -+ 'token_endpoint_auth_method', -+ 'token_endpoint_auth_signing_alg', -+ 'userinfo_signed_response_alg'} -diff --git a/tests/test_client_04_service.py b/tests/test_client_04_service.py -index 95e0934..2eec542 100644 ---- a/tests/test_client_04_service.py -+++ b/tests/test_client_04_service.py -@@ -62,6 +62,7 @@ class TestService: - "jwks", - "redirect_uris", - "request_object_signing_alg", -+ 'request_parameter_supported', - "response_modes", - "response_types", - "scope", -diff --git a/tests/test_client_05_util.py b/tests/test_client_05_util.py -index 3a22416..057c954 100644 ---- a/tests/test_client_05_util.py -+++ b/tests/test_client_05_util.py -@@ -7,6 +7,7 @@ from urllib.parse import urlsplit - import pytest - - from idpyoidc.client.exception import WrongContentType -+from idpyoidc.client.util import get_content_type - from idpyoidc.client.util import get_deserialization_method - from idpyoidc.client.util import get_http_body - from idpyoidc.client.util import get_http_url -@@ -139,28 +140,35 @@ def test_verify_header(): - - def test_get_deserialization_method_json(): - resp = FakeResponse("application/json") -- assert get_deserialization_method(resp) == "json" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "json" - - resp = FakeResponse("application/json; charset=utf-8") -- assert get_deserialization_method(resp) == "json" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "json" - - resp.headers["content-type"] = "application/jrd+json" -- assert get_deserialization_method(resp) == "json" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "json" - - - def test_get_deserialization_method_jwt(): - resp = FakeResponse("application/jwt") -- assert get_deserialization_method(resp) == "jwt" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "jwt" - - - def test_get_deserialization_method_urlencoded(): - resp = FakeResponse(URL_ENCODED) -- assert get_deserialization_method(resp) == "urlencoded" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "urlencoded" - - - def test_get_deserialization_method_text(): - resp = FakeResponse("text/html") -- assert get_deserialization_method(resp) == "" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "" - - resp = FakeResponse("text/plain") -- assert get_deserialization_method(resp) == "" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "" -diff --git a/tests/test_client_06_client_authn.py b/tests/test_client_06_client_authn.py -index 52fb95c..6a42ae5 100644 ---- a/tests/test_client_06_client_authn.py -+++ b/tests/test_client_06_client_authn.py -@@ -3,26 +3,28 @@ import os - - import pytest - from cryptojwt.exception import MissingKey --from cryptojwt.jws.jws import JWS - from cryptojwt.jws.jws import factory -+from cryptojwt.jws.jws import JWS - from cryptojwt.jwt import JWT - from cryptojwt.key_bundle import KeyBundle --from cryptojwt.key_jar import KeyJar - from cryptojwt.key_jar import init_key_jar -+from cryptojwt.key_jar import KeyJar - - from idpyoidc.claims import Claims -+from idpyoidc.client.client_auth import assertion_jwt - from idpyoidc.client.client_auth import AuthnFailure -+from idpyoidc.client.client_auth import bearer_auth - from idpyoidc.client.client_auth import BearerBody - from idpyoidc.client.client_auth import BearerHeader - from idpyoidc.client.client_auth import ClientSecretBasic - from idpyoidc.client.client_auth import ClientSecretJWT - from idpyoidc.client.client_auth import ClientSecretPost - from idpyoidc.client.client_auth import PrivateKeyJWT --from idpyoidc.client.client_auth import assertion_jwt --from idpyoidc.client.client_auth import bearer_auth - from idpyoidc.client.client_auth import valid_service_context - from idpyoidc.client.entity import Entity - from idpyoidc.defaults import JWT_BEARER -+from idpyoidc.key_import import add_kb -+from idpyoidc.key_import import import_jwks - from idpyoidc.message import Message - from idpyoidc.message.oauth2 import AccessTokenRequest - from idpyoidc.message.oauth2 import AccessTokenResponse -@@ -88,12 +90,13 @@ def test_quote(): - ) - - assert ( -- http_args["headers"]["Authorization"] == "Basic " -- "Nzk2ZDhmYWUtYTQyZi00ZTRmLWFiMjUtZDYyMDViNmQ0ZmEyOk1LRU0vQTdQa243SnVVMExBY3h5SFZLdndkY3pzdWdhUFUwQmllTGI0Q2JRQWdRait5cGNhbkZPQ2IwL0ZBNWg=" -+ http_args["headers"]["Authorization"] == "Basic " -+ "Nzk2ZDhmYWUtYTQyZi00ZTRmLWFiMjUtZDYyMDViNmQ0ZmEyOk1LRU0vQTdQa243SnVVMExBY3h5SFZLdndkY3pzdWdhUFUwQmllTGI0Q2JRQWdRait5cGNhbkZPQ2IwL0ZBNWg=" - ) - - - class TestClientSecretBasic(object): -+ - def test_construct(self, entity): - _service = entity.get_service("") - request = _service.construct( -@@ -127,6 +130,7 @@ class TestClientSecretBasic(object): - - - class TestBearerHeader(object): -+ - def test_construct(self, entity): - request = ResourceRequest(access_token="Sesame") - bh = BearerHeader() -@@ -196,6 +200,7 @@ class TestBearerHeader(object): - - - class TestBearerBody(object): -+ - def test_construct(self, entity): - _token_service = entity.get_service("") - request = ResourceRequest(access_token="Sesame") -@@ -252,6 +257,7 @@ class TestBearerBody(object): - - - class TestClientSecretPost(object): -+ - def test_construct(self, entity): - _token_service = entity.get_service("") - request = _token_service.construct( -@@ -292,6 +298,7 @@ class TestClientSecretPost(object): - - - class TestPrivateKeyJWT(object): -+ - def test_construct(self, entity): - token_service = entity.get_service("") - kb_rsa = KeyBundle( -@@ -320,8 +327,8 @@ class TestPrivateKeyJWT(object): - - # Receiver - _kj = KeyJar() -- _kj.import_jwks(_keyjar.export_jwks(), issuer_id=_context.get_client_id()) -- _kj.add_kb(_context.get_client_id(), kb_rsa) -+ _kj = import_jwks(_kj, _keyjar.export_jwks(), _context.get_client_id()) -+ _kj = add_kb(_kj, kb_rsa, _context.get_client_id()) - jso = JWT(key_jar=_kj).unpack(cas) - assert _eq(jso.keys(), ["aud", "iss", "sub", "jti", "exp", "iat"]) - # assert _jwt.headers == {'alg': 'RS256'} -@@ -350,6 +357,7 @@ class TestPrivateKeyJWT(object): - - - class TestClientSecretJWT_TE(object): -+ - def test_client_secret_jwt(self, entity): - _service_context = entity.get_context() - _service_context.token_endpoint = "https://example.com/token" -@@ -487,6 +495,7 @@ class TestClientSecretJWT_TE(object): - - - class TestClientSecretJWT_UI(object): -+ - def test_client_secret_jwt(self, entity): - access_token_service = entity.get_service("") - -@@ -526,6 +535,7 @@ class TestClientSecretJWT_UI(object): - - - class TestValidClientInfo(object): -+ - def test_valid_service_context(self, entity): - _service_context = entity.get_context() - -diff --git a/tests/test_client_16_util.py b/tests/test_client_16_util.py -index a09d65a..57c4bf6 100644 ---- a/tests/test_client_16_util.py -+++ b/tests/test_client_16_util.py -@@ -12,6 +12,7 @@ from idpyoidc.client import util - from idpyoidc.client.exception import WrongContentType - from idpyoidc.client.util import JSON_ENCODED - from idpyoidc.client.util import URL_ENCODED -+from idpyoidc.client.util import get_content_type - from idpyoidc.client.util import get_deserialization_method - from idpyoidc.message.oauth2 import AccessTokenRequest - from idpyoidc.message.oauth2 import AuthorizationRequest -@@ -145,31 +146,38 @@ def test_verify_header(): - - def test_get_deserialization_method_json(): - resp = FakeResponse("application/json") -- assert get_deserialization_method(resp) == "json" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "json" - - resp = FakeResponse("application/json; charset=utf-8") -- assert get_deserialization_method(resp) == "json" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "json" - - resp.headers["content-type"] = "application/jrd+json" -- assert get_deserialization_method(resp) == "json" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "json" - - - def test_get_deserialization_method_jwt(): - resp = FakeResponse("application/jwt") -- assert get_deserialization_method(resp) == "jwt" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "jwt" - - - def test_get_deserialization_method_urlencoded(): - resp = FakeResponse(URL_ENCODED) -- assert get_deserialization_method(resp) == "urlencoded" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "urlencoded" - - - def test_get_deserialization_method_text(): - resp = FakeResponse("text/html") -- assert get_deserialization_method(resp) == "" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "" - - resp = FakeResponse("text/plain") -- assert get_deserialization_method(resp) == "" -+ ctype = get_content_type(resp) -+ assert get_deserialization_method(ctype) == "" - - - def test_verify_no_content_type(): -diff --git a/tests/test_client_20_oauth2.py b/tests/test_client_20_oauth2.py -index 5e5df3f..336ba13 100644 ---- a/tests/test_client_20_oauth2.py -+++ b/tests/test_client_20_oauth2.py -@@ -191,6 +191,6 @@ class TestClient2(object): - - def test_keyjar(self): - _keyjar = self.client.get_attribute("keyjar") -- assert len(_keyjar) == 2 # one issuer -- assert len(_keyjar[""]) == 3 -- assert len(_keyjar.get("sig")) == 3 -+ assert len(_keyjar) == 1 # -+ assert len(_keyjar[""]) == 2 -+ assert len(_keyjar.get("sig")) == 2 -diff --git a/tests/test_client_21_oidc_service.py b/tests/test_client_21_oidc_service.py -index 5eba310..ca1dfb6 100644 ---- a/tests/test_client_21_oidc_service.py -+++ b/tests/test_client_21_oidc_service.py -@@ -1,19 +1,22 @@ - import os - -+import pytest -+import responses - from cryptojwt.exception import UnsupportedAlgorithm - from cryptojwt.jws import jws - from cryptojwt.jws.utils import left_hash - from cryptojwt.jwt import JWT - from cryptojwt.key_jar import build_keyjar - from cryptojwt.key_jar import init_key_jar --import pytest --import responses - - from idpyoidc.client.defaults import DEFAULT_OIDC_SERVICES - from idpyoidc.client.entity import Entity - from idpyoidc.client.exception import ParameterError - from idpyoidc.client.oidc.registration import response_types_to_grant_types - from idpyoidc.exception import MissingRequiredAttribute -+from idpyoidc.key_import import import_jwks -+from idpyoidc.key_import import import_jwks_from_file -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AccessTokenResponse - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB -@@ -30,6 +33,7 @@ from idpyoidc.message.oidc.session import EndSessionRequest - - - class Response(object): -+ - def __init__(self, status_code, text, headers=None): - self.status_code = status_code - self.text = text -@@ -45,15 +49,19 @@ _dirname = os.path.dirname(os.path.abspath(__file__)) - - ISS = "https://example.com" - --ISS_KEY = init_key_jar( -- public_path="{}/pub_iss.jwks".format(_dirname), -- private_path="{}/priv_iss.jwks".format(_dirname), -- key_defs=KEYSPEC, -- issuer_id=ISS, -- read_only=False, --) -- --ISS_KEY.import_jwks_as_json(open("{}/pub_client.jwks".format(_dirname)).read(), "client_id") -+# Issuers keys -+def issuers_keyjar(): -+ _keyjar = init_key_jar( -+ public_path="{}/pub_iss.jwks".format(_dirname), -+ private_path="{}/priv_iss.jwks".format(_dirname), -+ key_defs=KEYSPEC, -+ issuer_id=ISS, -+ read_only=False, -+ ) -+ -+ # add clients keys -+ _keyjar = import_jwks_from_file(_keyjar, f"{_dirname}/pub_client.jwks", "client_id") -+ return _keyjar - - - def make_keyjar(): -@@ -64,12 +72,12 @@ def make_keyjar(): - issuer_id="client_id", - read_only=False, - ) -- _keyjar.import_jwks(_keyjar.export_jwks(private=True, issuer_id="client_id"), issuer_id="") -- _keyjar.import_jwks_as_json(open("{}/pub_iss.jwks".format(_dirname)).read(), ISS) -+ _keyjar = store_under_other_id(_keyjar, "client_id", "", True) - return _keyjar - - - class TestAuthorization(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - client_config = { -@@ -94,6 +102,8 @@ class TestAuthorization(object): - _context.issuer = "https://example.com" - _context.map_supported_to_preferred() - _context.map_preferred_to_registered() -+ # Add the servers keys -+ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) - self.context = _context - self.service = entity.get_service("authorization") - -@@ -201,7 +211,7 @@ class TestAuthorization(object): - _jws = jws.factory(msg["request"]) - assert _jws - _resp = _jws.verify_compact( -- msg["request"], keys=ISS_KEY.get_signing_key(key_type="RSA", issuer_id="client_id") -+ msg["request"], keys=issuers_keyjar().get_signing_key(key_type="RSA", issuer_id="client_id") - ) - assert _resp - assert set(_resp.keys()) == { -@@ -214,6 +224,8 @@ class TestAuthorization(object): - "iss", - "aud", - "iat", -+ "jti", -+ "exp" - } - - def test_request_param(self): -@@ -245,7 +257,7 @@ class TestAuthorization(object): - self.service.endpoint = "https://example.com/authorize" - _info = self.service.get_request_parameters(request_args=req_args) - # Build an ID Token -- idt = JWT(key_jar=ISS_KEY, iss=ISS, lifetime=3600) -+ idt = JWT(key_jar=issuers_keyjar(), iss=ISS, lifetime=3600) - payload = {"sub": "123456789", "aud": ["client_id"], "nonce": "nonce"} - # have to calculate c_hash - alg = "RS256" -@@ -262,7 +274,7 @@ class TestAuthorization(object): - self.service.endpoint = "https://example.com/authorize" - _info = self.service.get_request_parameters(request_args=req_args) - # Build an ID Token -- idt = JWT(ISS_KEY, iss=ISS, lifetime=3600) -+ idt = JWT(issuers_keyjar(), iss=ISS, lifetime=3600) - payload = {"sub": "123456789", "aud": ["client_id"], "nonce": "noice"} - # have to calculate c_hash - alg = "RS256" -@@ -279,7 +291,7 @@ class TestAuthorization(object): - self.service.endpoint = "https://example.com/authorize" - self.service.get_request_parameters(request_args=req_args) - # Build an ID Token -- idt = JWT(ISS_KEY, iss=ISS, lifetime=3600) -+ idt = JWT(issuers_keyjar(), iss=ISS, lifetime=3600) - payload = {"sub": "123456789", "aud": ["client_id"]} - # have to calculate c_hash - alg = "RS256" -@@ -297,7 +309,7 @@ class TestAuthorization(object): - self.service.endpoint = "https://example.com/authorize" - self.service.get_request_parameters(request_args=req_args) - # Build an ID Token -- idt = JWT(ISS_KEY, iss=ISS, lifetime=3600, sign_alg="none") -+ idt = JWT(issuers_keyjar(), iss=ISS, lifetime=3600, sign_alg="none") - payload = {"sub": "123456789", "aud": ["client_id"], "nonce": req_args["nonce"]} - _idt = idt.pack(payload) - self.service.upstream_get("context").claims.set_usage( -@@ -312,6 +324,7 @@ class TestAuthorization(object): - - - class TestAuthorizationCallback(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - client_config = { -@@ -335,7 +348,7 @@ class TestAuthorizationCallback(object): - _context.issuer = "https://example.com" - _context.map_supported_to_preferred() - _context.map_preferred_to_registered() -- -+ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) - self.service = entity.get_service("authorization") - - def test_construct_code(self): -@@ -397,6 +410,7 @@ class TestAuthorizationCallback(object): - - - class TestAccessTokenRequest(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - client_config = { -@@ -410,6 +424,7 @@ class TestAccessTokenRequest(object): - _context.issuer = "https://example.com" - _context.provider_info = {"token_endpoint": f"{_context.issuer}/token"} - self.service = entity.get_service("accesstoken") -+ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) - - # add some history - auth_request = AuthorizationRequest( -@@ -475,6 +490,7 @@ class TestAccessTokenRequest(object): - - - class TestProviderInfo(object): -+ - @pytest.fixture(autouse=True) - def create_service(self): - self._iss = ISS -@@ -729,7 +745,8 @@ class TestProviderInfo(object): - # assert _context.claims.use == {} - resp = self.service.post_parse_response(provider_info_response) - -- iss_jwks = ISS_KEY.export_jwks_as_json(issuer_id=ISS) -+ iss_jwks = issuers_keyjar().export_jwks_as_json(issuer_id=ISS) -+ - with responses.RequestsMock() as rsps: - rsps.add("GET", resp["jwks_uri"], body=iss_jwks, status=200) - -@@ -739,9 +756,9 @@ class TestProviderInfo(object): - _context.map_preferred_to_registered() - - use_copy = self.service.upstream_get("context").claims.use.copy() -- # jwks content will change dynamically between runs -- assert "jwks" in use_copy -- del use_copy["jwks"] -+ if "jwks" in use_copy: -+ assert True -+ del use_copy["jwks"] - del use_copy["callback_uris"] - - assert use_copy == { -@@ -760,6 +777,7 @@ class TestProviderInfo(object): - "post_logout_redirect_uris": ["https://rp.example.com/post"], - "redirect_uris": ["https://example.com/cli/authz_cb"], - "request_object_signing_alg": "ES256", -+ 'request_parameter_supported': True, - "response_modes": ["query", "fragment", "form_post"], - "response_types": ["code"], - "scope": ["openid"], -@@ -807,6 +825,7 @@ class TestProviderInfo(object): - 'post_logout_redirect_uris', - 'redirect_uris', - 'request_object_signing_alg', -+ 'request_parameter_supported', - 'response_modes', - 'response_types', - 'scope', -@@ -816,7 +835,7 @@ class TestProviderInfo(object): - 'userinfo_signed_response_alg'} - resp = self.service.post_parse_response(provider_info_response) - -- iss_jwks = ISS_KEY.export_jwks_as_json(issuer_id=ISS) -+ iss_jwks = issuers_keyjar().export_jwks_as_json(issuer_id=ISS) - with responses.RequestsMock() as rsps: - rsps.add("GET", resp["jwks_uri"], body=iss_jwks, status=200) - -@@ -847,6 +866,7 @@ class TestProviderInfo(object): - "post_logout_redirect_uris": ["https://rp.example.com/post"], - "redirect_uris": ["https://example.com/cli/authz_cb"], - "request_object_signing_alg": "ES256", -+ 'request_parameter_supported': True, - "response_modes": ["query", "fragment", "form_post"], - "response_types": ["code"], - "scope": ["openid"], -@@ -872,11 +892,12 @@ def create_jws(val): - idts = IdToken(**val) - - return idts.to_jwt( -- key=ISS_KEY.get_signing_key("ec", issuer_id=ISS), algorithm="ES256", lifetime=lifetime -+ key=issuers_keyjar().get_signing_key("ec", issuer_id=ISS), algorithm="ES256", lifetime=lifetime - ) - - - class TestRegistration(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - self._iss = ISS -@@ -892,9 +913,11 @@ class TestRegistration(object): - services=DEFAULT_OIDC_SERVICES, - client_type="oidc", - ) -- entity.get_context().issuer = "https://example.com" -- entity.get_context().map_supported_to_preferred() -+ _context = entity.get_context() -+ _context.issuer = "https://example.com" -+ _context.map_supported_to_preferred() - self.service = entity.get_service("registration") -+ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) - - def test_construct(self): - _req = self.service.construct() -@@ -1040,6 +1063,7 @@ def test_config_logout_uri(): - - - class TestUserInfo(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - self._iss = ISS -@@ -1060,6 +1084,9 @@ class TestUserInfo(object): - entity.get_context().issuer = "https://example.com" - self.service = entity.get_service("userinfo") - -+ _context = entity.get_context() -+ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) -+ - entity.get_context().claims.use = { - "userinfo_signed_response_alg": "RS256", - "userinfo_encrypted_response_alg": "RSA-OAEP", -@@ -1074,7 +1101,9 @@ class TestUserInfo(object): - idtval = {"nonce": "KUEYfRM2VzKDaaKD", "sub": "diana", "iss": ISS, "aud": "client_id"} - idt = create_jws(idtval) - -- ver_idt = IdToken().from_jwt(idt, make_keyjar()) -+ _keyjar = make_keyjar() -+ _keyjar = import_jwks_from_file(_keyjar, f"{_dirname}/pub_iss.jwks", ISS) -+ ver_idt = IdToken().from_jwt(idt, _keyjar) - - token_response = AccessTokenResponse( - access_token="access_token", id_token=idt, __verified_id_token=ver_idt -@@ -1104,7 +1133,7 @@ class TestUserInfo(object): - "phone_number": "+1 (555) 123-4567", - } - -- srv = JWT(ISS_KEY, iss=ISS, sign_alg="ES256") -+ srv = JWT(issuers_keyjar(), iss=ISS, sign_alg="ES256") - _jwt = srv.pack(payload=claims) - - resp = OpenIDSchema( -@@ -1157,7 +1186,7 @@ class TestUserInfo(object): - - def test_unpack_signed_response(self): - resp = OpenIDSchema(sub="diana", given_name="Diana", family_name="krall", iss=ISS) -- sk = ISS_KEY.get_signing_key("rsa", issuer_id=ISS) -+ sk = issuers_keyjar().get_signing_key("rsa", issuer_id=ISS) - alg = self.service.upstream_get("context").get_sign_alg("userinfo") - _resp = self.service.parse_response( - resp.to_jwt(sk, algorithm=alg), state="abcde", sformat="jwt" -@@ -1168,16 +1197,18 @@ class TestUserInfo(object): - # Add encryption key - _kj = build_keyjar([{"type": "RSA", "use": ["enc"]}], issuer_id="") - # Own key jar gets the private key -- self.service.upstream_get("attribute", "keyjar").import_jwks( -- _kj.export_jwks(private=True), issuer_id="client_id" -- ) -- # opponent gets the public key -- ISS_KEY.import_jwks(_kj.export_jwks(), issuer_id="client_id") -+ _keyjar = self.service.upstream_get("attribute", "keyjar") -+ _keyjar = import_jwks(_keyjar, -+ _kj.export_jwks(private=True), -+ "client_id") -+ # opponent gets the client public keys -+ _keyjar = issuers_keyjar() -+ _keyjar = import_jwks(_keyjar, _kj.export_jwks(), "client_id") - - resp = OpenIDSchema( - sub="diana", given_name="Diana", family_name="krall", iss=ISS, aud="client_id" - ) -- enckey = ISS_KEY.get_encrypt_key("rsa", issuer_id="client_id") -+ enckey = _keyjar.get_encrypt_key("rsa", issuer_id="client_id") - algspec = self.service.upstream_get("context").get_enc_alg_enc(self.service.service_name) - - enc_resp = resp.to_jwe(enckey, **algspec) -@@ -1186,6 +1217,7 @@ class TestUserInfo(object): - - - class TestCheckSession(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - self._iss = ISS -@@ -1213,6 +1245,7 @@ class TestCheckSession(object): - - - class TestCheckID(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - self._iss = ISS -@@ -1240,6 +1273,7 @@ class TestCheckID(object): - - - class TestEndSession(object): -+ - @pytest.fixture(autouse=True) - def create_request(self): - self._iss = ISS -diff --git a/tests/test_client_24_oic_utils.py b/tests/test_client_24_oic_utils.py -index 4e79980..e7d177b 100644 ---- a/tests/test_client_24_oic_utils.py -+++ b/tests/test_client_24_oic_utils.py -@@ -1,8 +1,8 @@ - from cryptojwt.jwe.jwe import factory - from cryptojwt.key_jar import build_keyjar - --from idpyoidc.client.oidc.utils import construct_request_uri --from idpyoidc.client.oidc.utils import request_object_encryption -+from idpyoidc.client.request_object import construct_request_uri -+from idpyoidc.client.request_object import request_object_encryption - from idpyoidc.client.service_context import ServiceContext - from idpyoidc.message.oidc import AuthorizationRequest - -diff --git a/tests/test_client_27_conversation.py b/tests/test_client_27_conversation.py -index fbb2239..a280400 100644 ---- a/tests/test_client_27_conversation.py -+++ b/tests/test_client_27_conversation.py -@@ -9,10 +9,11 @@ from cryptojwt.key_jar import KeyJar - - from idpyoidc.client.entity import Entity - from idpyoidc.client.oidc.webfinger import WebFinger --from idpyoidc.message.oidc import JRD -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oidc import AccessTokenResponse - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - from idpyoidc.message.oidc import AuthorizationResponse -+from idpyoidc.message.oidc import JRD - from idpyoidc.message.oidc import Link - from idpyoidc.message.oidc import OpenIDSchema - from idpyoidc.message.oidc import ProviderConfigurationResponse -@@ -28,20 +29,20 @@ JWKS_OP = { - "keys": [ - { - "d": "mcAW1xeNsjzyV1M7F7_cUHz0MIR" -- "-tcnKFJnbbo5UXxMRUPu17qwRHr8ttep1Ie64r2L9QlphcT9BjYd0KQ8ll3flIzLtiJv__MNPQVjk5bsYzb_erQRzSwLJU-aCcNFB8dIyQECzu-p44UVEPQUGzykImsSShvMQhcvrKiqqg7NlijJuEKHaKynV9voPsjwKYSqk6lH8kMloCaVS-dOkK-r7bZtbODUxx9GJWnxhX0JWXcdrPZRb29y9cdthrMcEaCXG23AxnMEfp-enDqarLHYTQrCBJXs_b-9k2d8v9zLm7E-Pf-0YGmaoJtX89lwQkO_SmFF3sXsnI2cFreqU3Q", -+ "-tcnKFJnbbo5UXxMRUPu17qwRHr8ttep1Ie64r2L9QlphcT9BjYd0KQ8ll3flIzLtiJv__MNPQVjk5bsYzb_erQRzSwLJU-aCcNFB8dIyQECzu-p44UVEPQUGzykImsSShvMQhcvrKiqqg7NlijJuEKHaKynV9voPsjwKYSqk6lH8kMloCaVS-dOkK-r7bZtbODUxx9GJWnxhX0JWXcdrPZRb29y9cdthrMcEaCXG23AxnMEfp-enDqarLHYTQrCBJXs_b-9k2d8v9zLm7E-Pf-0YGmaoJtX89lwQkO_SmFF3sXsnI2cFreqU3Q", - "e": "AQAB", - "kid": "c19uYlBJXzVfNjNZeGVnYmxncHZwUzZTZDVwUFdxdVJLU3AxQXdwaFdfbw", - "kty": "RSA", - "n": "3ZblhNL2CjRktLM9vyDn8jnA4G1B1HCpPh" -- "-gv2AK4m9qDBZPYZGOGqzeW3vanvLTBlqnPm0GHg4rOrfMEwwLrfMcgmg1y4GD0vVU8G9HP1" -- "-oUPtKUqaKOp313tFKzFh9_OHGQ6EmhxG7gegPR9kQXduTDXqBFi81MzRplIQ8DHLM3-n2CyDW1V" -- "-dhRVh" -- "-AM0ZcJyzR_DvZ3mhG44DysPdHQOSeWnpdn1d81" -- "-PriqZfhAF9tn1ihgtjXd5swf1HTSjLd7xv1hitGf2245Xmr" -- "-V2pQFzeMukLM3JKbTYbElsB7Zm0wZx49hZMtgx35XMoO04bifdbO3yLtTA5ovXN3fQ", -+ "-gv2AK4m9qDBZPYZGOGqzeW3vanvLTBlqnPm0GHg4rOrfMEwwLrfMcgmg1y4GD0vVU8G9HP1" -+ "-oUPtKUqaKOp313tFKzFh9_OHGQ6EmhxG7gegPR9kQXduTDXqBFi81MzRplIQ8DHLM3-n2CyDW1V" -+ "-dhRVh" -+ "-AM0ZcJyzR_DvZ3mhG44DysPdHQOSeWnpdn1d81" -+ "-PriqZfhAF9tn1ihgtjXd5swf1HTSjLd7xv1hitGf2245Xmr" -+ "-V2pQFzeMukLM3JKbTYbElsB7Zm0wZx49hZMtgx35XMoO04bifdbO3yLtTA5ovXN3fQ", - "p": "88aNu59aBn0elksaVznzoVKkdbT5B4euhOIEqJoFvFbEocw9mC4k" -- "-yozIAQSV5FEakoSPOl8lrymCoM3Q1fVHfaM9Rbb9RCRlsV1JOeVVZOE05HUdz8zOIqLBDEGM_oQqDwF_kp" -- "-4nDTZ1-dtnGdTo4Cf7QRuApzE_dwVabUCTc", -+ "-yozIAQSV5FEakoSPOl8lrymCoM3Q1fVHfaM9Rbb9RCRlsV1JOeVVZOE05HUdz8zOIqLBDEGM_oQqDwF_kp" -+ "-4nDTZ1-dtnGdTo4Cf7QRuApzE_dwVabUCTc", - "q": "6LOHuM7H_0kDrMTwUEX7Aubzr792GoJ6EgTKIQY25SAFTZpYwuC3NnqlAdy8foIa3d7eGU2yICRbBG0S_ITcooDFrOa7nZ6enMUclMTxW8FwwvBXeIHo9cIsrKYtOThGplz43Cvl73MK5M58ZRmuhaNYa6Mk4PL4UokARfEiDus", - "use": "sig", - }, -@@ -58,7 +59,7 @@ JWKS_OP = { - } - - OP_KEYJAR = KeyJar() --OP_KEYJAR.import_jwks(JWKS_OP, "") -+OP_KEYJAR = import_jwks(OP_KEYJAR, JWKS_OP, "") - OP_PUBLIC_JWKS = OP_KEYJAR.export_jwks() - OP_BASEURL = "https://example.org/op" - -@@ -70,13 +71,13 @@ RP_JWKS = { - "kid": "Mk0yN2w0N3BZLWtyOEpQWGFmNDZvQi1hbDl2azR3ai1WNElGdGZQSFd6MA", - "e": "AQAB", - "n": "yPrOADZtGoa9jxFCmDsJ1nAYmzgznUxCtUlb_ty33" -- "-AFNEqzW_pSLr5g6RQAPGsvVQqbsb9AB18QNgz" -- "-eG7cnvKIIR7JXWCuGv_Q9MwoRD0-zaYGRbRvFoTZokZMB6euBfMo6kijJ" -- "-gdKuSaxIE84X_Fcf1ESAKJ0EX6Cxdm8hKkBelGIDPMW5z7EHQ8OuLCQtTJnDvbjEOk9sKzkKqVj53XFs5vjd4WUhxS6xIDcWE-lTafUpm0BsobklLePidHxyAMGOunL_Pt3RCLZGlWeWOO9fZhLtydiDWiZlcNR0FQEX_mfV1kCOHHBFN1VKOY2pyJpjp9djdtHxPZ9fP35w", -+ "-AFNEqzW_pSLr5g6RQAPGsvVQqbsb9AB18QNgz" -+ "-eG7cnvKIIR7JXWCuGv_Q9MwoRD0-zaYGRbRvFoTZokZMB6euBfMo6kijJ" -+ "-gdKuSaxIE84X_Fcf1ESAKJ0EX6Cxdm8hKkBelGIDPMW5z7EHQ8OuLCQtTJnDvbjEOk9sKzkKqVj53XFs5vjd4WUhxS6xIDcWE-lTafUpm0BsobklLePidHxyAMGOunL_Pt3RCLZGlWeWOO9fZhLtydiDWiZlcNR0FQEX_mfV1kCOHHBFN1VKOY2pyJpjp9djdtHxPZ9fP35w", - "d": "aRBTqGDLYFaXuba4LYSPe_5Vnq8erFg1dzfGU9Fmfi5KCjAS2z5cv_reBnpiNTODJt3Izn7AJhpYCyl3zdWGl8EJ0OabNalY2txoi9A-LI4nyrHEDaRpfkgszVwaWtYZbxrShMc8I5x_wvCGx7sX7Hoy6YgQreRFzw8Fy86MDncpmcUwQTnXVUMLgioeYz5gW6rwXkqj_NVyuHPiheykJG026cXFNBWplCk4ET1bvf_6ZB9QmLwO16Pu2O-dtu1HHDOqI7y6-YgKIC6mcLrQrF9-FO7NkilcOB7zODNiYzhDBQ2YJAbcdn_3M_lkhaFwR-n4WB7vCM0vNqz7lEg6QQ", - "p": "_STNoJFkX9_uw8whytVmTrHP5K7vcZBIH9nuCTvj137lC48ZpR1UARx4qShxHLfK7DrufHd7TYnJkEMNUHFmdKvkaVQMY0_BsBSvCrUl10gzxsI08hg53L17E1Pe73iZp3f5nA4eB-1YB-km1Cc-Xs10OPWedJHf9brlCPDLAb8", - "q": "yz9T0rPEc0ZPjSi45gsYiQL2KJ3UsPHmLrgOHq0D4UvsB6UFtUtOWh7A1UpQdmBuHjIJz" -- "-Iq7VH4kzlI6VxoXhwE69oxBXr4I7fBudZRvlLuIJS9M2wvsTVouj0DBYSR6ZlAQHCCou89P2P6zQCEaqu7bWXNcpyTixbbvOU1w9k", -+ "-Iq7VH4kzlI6VxoXhwE69oxBXr4I7fBudZRvlLuIJS9M2wvsTVouj0DBYSR6ZlAQHCCou89P2P6zQCEaqu7bWXNcpyTixbbvOU1w9k", - }, - { - "kty": "EC", -@@ -91,12 +92,12 @@ RP_JWKS = { - } - - RP_KEYJAR = KeyJar() --RP_KEYJAR.import_jwks(RP_JWKS, "") --RP_KEYJAR.import_jwks(OP_PUBLIC_JWKS, OP_BASEURL) -+RP_KEYJAR = import_jwks(RP_KEYJAR, RP_JWKS, "") -+RP_KEYJAR = import_jwks(RP_KEYJAR, OP_PUBLIC_JWKS, OP_BASEURL) - RP_BASEURL = "https://example.com/rp" - - SERVICE_PUBLIC_JWKS = RP_KEYJAR.export_jwks("") --OP_KEYJAR.import_jwks(SERVICE_PUBLIC_JWKS, RP_BASEURL) -+OP_KEYJAR = import_jwks(OP_KEYJAR, SERVICE_PUBLIC_JWKS, RP_BASEURL) - - # --------------------------------------------------- - -@@ -155,11 +156,11 @@ def test_conversation(): - info = webfinger_service.get_request_parameters(request_args={"resource": "foobar@example.org"}) - - assert ( -- info["url"] == "https://example.org/.well-known/webfinger?rel=http" -- "%3A%2F" -- "%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer" -- "&resource" -- "=acct%3Afoobar%40example.org" -+ info["url"] == "https://example.org/.well-known/webfinger?rel=http" -+ "%3A%2F" -+ "%2Fopenid.net%2Fspecs%2Fconnect%2F1.0%2Fissuer" -+ "&resource" -+ "=acct%3Afoobar%40example.org" - ) - - webfinger_response = json.dumps( -diff --git a/tests/test_client_28_stand_alone.py b/tests/test_client_28_stand_alone.py -index d3c3a2a..0ea921a 100644 ---- a/tests/test_client_28_stand_alone.py -+++ b/tests/test_client_28_stand_alone.py -@@ -11,6 +11,7 @@ from idpyoidc.client.defaults import OIDCONF_PATTERN - from idpyoidc.client.exception import Unsupported - from idpyoidc.client.oauth2.stand_alone_client import StandAloneClient - from idpyoidc.exception import VerificationError -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oidc import AccessTokenResponse - from idpyoidc.message.oidc import AuthorizationResponse - from idpyoidc.message.oidc import IdToken -@@ -416,7 +417,7 @@ class TestPostAuthn(object): - idval = {"nonce": _nonce, "sub": subject, "iss": _iss, "aud": _aud} - - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(ISSUER_KEYS.export_jwks(issuer_id=ISSUER), ISSUER) -+ _keyjar = import_jwks(_keyjar, ISSUER_KEYS.export_jwks(issuer_id=ISSUER), ISSUER) - - idts = IdToken(**idval) - return idts.to_jwt( -diff --git a/tests/test_client_30_rp_handler_oidc.py b/tests/test_client_30_rp_handler_oidc.py -index 3a3d75f..f02aa8d 100644 ---- a/tests/test_client_30_rp_handler_oidc.py -+++ b/tests/test_client_30_rp_handler_oidc.py -@@ -4,12 +4,13 @@ from urllib.parse import parse_qs - from urllib.parse import urlparse - from urllib.parse import urlsplit - --from cryptojwt.key_jar import init_key_jar - import pytest - import responses -+from cryptojwt.key_jar import init_key_jar - - from idpyoidc.client.entity import Entity - from idpyoidc.client.rp_handler import RPHandler -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oidc import AccessTokenResponse - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - from idpyoidc.message.oidc import AuthorizationResponse -@@ -217,6 +218,7 @@ def iss_id(iss): - - - class TestRPHandler(object): -+ - @pytest.fixture(autouse=True) - def rphandler_setup(self): - self.rph = RPHandler( -@@ -270,6 +272,7 @@ class TestRPHandler(object): - 'id_token_signing_alg_values_supported', - 'redirect_uris', - 'request_object_signing_alg_values_supported', -+ 'request_parameter_supported', - 'response_modes_supported', - 'response_types_supported', - 'scopes_supported', -@@ -279,13 +282,13 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - # The key jar should only contain a symmetric key that is the clients - # secret. 2 because one is marked for encryption and the other signing - # usage. - -- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} -+ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} - keys = _keyjar.get_issuer_keys("") - assert len(keys) == 3 - -@@ -329,9 +332,9 @@ class TestRPHandler(object): - assert _context.issuer == _github_id - - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - -- assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} -+ assert set(_keyjar.owners()) == {"", _context.claims.prefer["client_id"], _github_id} - keys = _keyjar.get_issuer_keys("") - assert len(keys) == 3 - -@@ -347,7 +350,7 @@ class TestRPHandler(object): - cb = _context.get_preference("callback_uris") - - assert set(cb.keys()) == {"request_uris", "redirect_uris"} -- assert set(cb["redirect_uris"].keys()) == {"query", "fragment"} -+ assert set(cb["redirect_uris"].keys()) == {"query", "fragment", "form_post"} - _hash = _context.iss_hash - - assert cb["redirect_uris"]["query"] == [f"https://example.com/rp/authz_cb/{_hash}"] -@@ -449,7 +452,7 @@ class TestRPHandler(object): - _github_id = iss_id("github") - _context = client.get_context() - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - _nonce = _session["nonce"] - _iss = _session["iss"] -@@ -524,7 +527,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -571,7 +574,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -618,7 +621,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -697,6 +700,7 @@ def test_get_provider_specific_service(): - - - class TestRPHandlerTier2(object): -+ - @pytest.fixture(autouse=True) - def rphandler_setup(self): - self.rph = RPHandler(BASE_URL, CLIENT_CONFIG, keyjar=CLI_KEY) -@@ -712,7 +716,7 @@ class TestRPHandlerTier2(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -818,6 +822,7 @@ class TestRPHandlerTier2(object): - - - class MockResponse: -+ - def __init__(self, status_code, text, headers=None): - self.status_code = status_code - self.text = text -@@ -825,6 +830,7 @@ class MockResponse: - - - class MockOP(object): -+ - def __init__(self, issuer, keyjar=None): - self.keyjar = keyjar - self.issuer = issuer -@@ -913,6 +919,7 @@ def test_rphandler_request(): - - - class TestRPHandlerWithMockOP(object): -+ - @pytest.fixture(autouse=True) - def rphandler_setup(self): - self.issuer = "https://github.com/login/oauth/authorize" -@@ -956,7 +963,7 @@ class TestRPHandlerWithMockOP(object): - ) - _github_id = iss_id("github") - _keyjar = client.get_attribute("keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - with responses.RequestsMock() as rsps: - rsps.add( - "POST", -diff --git a/tests/test_client_30_rph_defaults.py b/tests/test_client_30_rph_defaults.py -index 177b772..7648572 100644 ---- a/tests/test_client_30_rph_defaults.py -+++ b/tests/test_client_30_rph_defaults.py -@@ -45,11 +45,11 @@ class TestRPHandler(object): - 'id_token_encryption_alg_values_supported', - 'id_token_encryption_enc_values_supported', - 'id_token_signing_alg_values_supported', -- 'jwks_uri', - 'redirect_uris', - 'request_object_encryption_alg_values_supported', - 'request_object_encryption_enc_values_supported', - 'request_object_signing_alg_values_supported', -+ 'request_parameter_supported', - 'response_modes_supported', - 'response_types_supported', - 'scopes_supported', -@@ -61,7 +61,7 @@ class TestRPHandler(object): - 'userinfo_signing_alg_values_supported'} - - _keyjar = client.get_attribute("keyjar") -- assert list(_keyjar.owners()) == ["", BASE_URL] -+ assert list(_keyjar.owners()) == [""] - keys = _keyjar.get_issuer_keys("") - assert len(keys) == 2 - -@@ -116,9 +116,9 @@ class TestRPHandler(object): - "encrypt_request_object_supported", - "grant_types", - "id_token_signed_response_alg", -- "jwks_uri", - "redirect_uris", - "request_object_signing_alg", -+ 'request_parameter_supported', - "response_modes", - "response_types", - "scope", -@@ -180,4 +180,5 @@ class TestRPHandler(object): - rsps.add("POST", request_uri, body=_jws, status=200) - self.rph.do_client_registration(client, ISS_ID) - -- assert "jwks_uri" in _context.get("registration_response") -+ assert "client_id" in _context.get("registration_response") -+ assert _context.client_id -diff --git a/tests/test_client_41_rp_handler_persistent.py b/tests/test_client_41_rp_handler_persistent.py -index 8edce03..d70dd1d 100644 ---- a/tests/test_client_41_rp_handler_persistent.py -+++ b/tests/test_client_41_rp_handler_persistent.py -@@ -6,6 +6,7 @@ import responses - from cryptojwt.key_jar import init_key_jar - - from idpyoidc.client.rp_handler import RPHandler -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oidc import AccessTokenResponse - from idpyoidc.message.oidc import APPLICATION_TYPE_WEB - from idpyoidc.message.oidc import AuthorizationResponse -@@ -249,7 +250,7 @@ class TestRPHandler(object): - assert _context.get("issuer") == _github_id - - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - assert set(_keyjar.owners()) == {"", "eeeeeeeee", _github_id} - keys = _keyjar.get_issuer_keys("") -@@ -291,7 +292,7 @@ class TestRPHandler(object): - assert query["client_id"] == ["eeeeeeeee"] - assert query["redirect_uri"] == ["https://example.com/rp/authz_cb/github"] - assert query["response_type"] == ["code"] -- assert query["scope"] == ["user public_repo openid"] -+ assert query["scope"] == ["openid"] - - def test_get_session_information(self): - rph_1 = RPHandler( -@@ -376,7 +377,7 @@ class TestRPHandler(object): - _github_id = iss_id("github") - _context = client.get_context() - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - _nonce = _session["nonce"] - _iss = _session["iss"] -@@ -457,7 +458,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -508,7 +509,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -@@ -559,7 +560,7 @@ class TestRPHandler(object): - - _github_id = iss_id("github") - _keyjar = _context.upstream_get("attribute", "keyjar") -- _keyjar.import_jwks(GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) -+ _keyjar = import_jwks(_keyjar, GITHUB_KEY.export_jwks(issuer_id=_github_id), _github_id) - - idts = IdToken(**idval) - _signed_jwt = idts.to_jwt( -diff --git a/tests/test_client_55_token_exchange.py b/tests/test_client_55_token_exchange.py -index 108e867..6fbc4ca 100644 ---- a/tests/test_client_55_token_exchange.py -+++ b/tests/test_client_55_token_exchange.py -@@ -4,6 +4,7 @@ import pytest - from cryptojwt.key_jar import init_key_jar - - from idpyoidc.client.entity import Entity -+from idpyoidc.key_import import import_jwks_from_file - from idpyoidc.message import Message - from idpyoidc.message.oauth2 import AccessTokenResponse - from idpyoidc.message.oauth2 import AuthorizationResponse -@@ -27,7 +28,7 @@ ISS_KEY = init_key_jar( - read_only=False, - ) - --ISS_KEY.import_jwks_as_json(open("{}/pub_client.jwks".format(_dirname)).read(), "client_id") -+ISS_KEY = import_jwks_from_file(ISS_KEY, f"{_dirname}/pub_client.jwks", "client_id") - - - def create_jws(val): -@@ -63,6 +64,9 @@ class TestUserInfo(object): - }, - ) - entity.get_context().issuer = "https://example.com" -+ _context = entity.get_context() -+ _context.keyjar = import_jwks_from_file(_context.keyjar, f"{_dirname}/pub_iss.jwks", ISS) -+ - self.service = entity.get_service("token_exchange") - _cstate = self.service.upstream_get("context").cstate - # Add history -@@ -72,7 +76,7 @@ class TestUserInfo(object): - idtval = {"nonce": "KUEYfRM2VzKDaaKD", "sub": "diana", "iss": ISS, "aud": "client_id"} - idt = create_jws(idtval) - -- ver_idt = IdToken().from_jwt(idt, make_keyjar()) -+ ver_idt = IdToken().from_jwt(idt, _context.keyjar) - - token_response = AccessTokenResponse( - access_token="access_token", id_token=idt, __verified_id_token=ver_idt -diff --git a/tests/test_server_05_token_handler.py b/tests/test_server_05_token_handler.py -index 21d247d..721cae7 100644 ---- a/tests/test_server_05_token_handler.py -+++ b/tests/test_server_05_token_handler.py -@@ -1,11 +1,6 @@ --import base64 --import hashlib --import hmac - import os --import secrets - - import pytest --from cryptojwt.jwe.fernet import FernetEncrypter - - from idpyoidc.encrypter import default_crypt_config - from idpyoidc.server import Server -@@ -39,6 +34,7 @@ def test_is_expired(): - - - class TestDefaultToken(object): -+ - @pytest.fixture(autouse=True) - def setup_token_handler(self): - password = "The longer the better. Is this close to enough ?" -@@ -78,6 +74,7 @@ class TestDefaultToken(object): - - - class TestTokenHandler(object): -+ - @pytest.fixture(autouse=True) - def setup_token_handler(self): - grant_expires_in = 600 -@@ -282,3 +279,87 @@ def test_file(jwks): - server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) - token_handler = server.context.session_manager.token_handler - assert token_handler -+ -+def test_token_handler_from_config_2(): -+ conf = { -+ "issuer": "https://example.com/op", -+ "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, -+ "endpoint": { -+ "endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}}, -+ }, -+ "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": { -+ "kwargs": { -+ "lifetime": 600, -+ "crypt_conf": { -+ "kwargs": { -+ "key": "0987654321abcdefghijklmnop...---", -+ "salt": "abcdefghijklmnop", -+ "iterations": 1 -+ } -+ } -+ } -+ }, -+ "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}, -+ } -+ }, -+ }, -+ }, -+ "session_params": SESSION_PARAMS, -+ } -+ -+ server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) -+ token_handler = server.context.session_manager.token_handler -+ assert token_handler -+ assert len(token_handler.handler) == 4 -+ assert set(token_handler.handler.keys()) == { -+ "authorization_code", -+ "access_token", -+ "refresh_token", -+ "id_token", -+ } -+ assert isinstance(token_handler.handler["authorization_code"], DefaultToken) -+ assert isinstance(token_handler.handler["access_token"], JWTToken) -+ assert isinstance(token_handler.handler["refresh_token"], JWTToken) -+ assert isinstance(token_handler.handler["id_token"], IDToken) -+ -+ assert token_handler.handler["authorization_code"].lifetime == 600 -+ -+ assert token_handler.handler["access_token"].alg == "ES256" -+ assert token_handler.handler["access_token"].kwargs == {"add_claims_by_scope": True} -+ assert token_handler.handler["access_token"].lifetime == 3600 -+ assert token_handler.handler["access_token"].def_aud == ["https://example.org/appl"] -+ -+ assert token_handler.handler["refresh_token"].alg == "ES256" -+ assert token_handler.handler["refresh_token"].kwargs == {} -+ assert token_handler.handler["refresh_token"].lifetime == 3600 -+ assert token_handler.handler["refresh_token"].def_aud == ["https://example.org/appl"] -+ -+ assert token_handler.handler["id_token"].lifetime == 300 -+ assert "base_claims" in token_handler.handler["id_token"].kwargs -+ -diff --git a/tests/test_server_08_id_token.py b/tests/test_server_08_id_token.py -index ecc72c6..5a9cb33 100644 ---- a/tests/test_server_08_id_token.py -+++ b/tests/test_server_08_id_token.py -@@ -6,6 +6,7 @@ from cryptojwt import JWT - from cryptojwt import KeyJar - from cryptojwt.jws.jws import factory - -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oidc import AuthorizationRequest - from idpyoidc.server import Server - from idpyoidc.server.authn_event import create_authn_event -@@ -160,6 +161,7 @@ USER_ID = "diana" - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_session_manager(self): - self.server = Server(conf) -@@ -467,7 +469,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) -@@ -501,7 +503,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - assert "nickname" in res -@@ -514,7 +516,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - -@@ -531,7 +533,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - -@@ -546,7 +548,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - assert "foobar" not in res -@@ -567,7 +569,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - assert "address" in res -@@ -586,7 +588,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - -@@ -605,7 +607,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - assert "address" in res -@@ -627,7 +629,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - assert "address" in res -@@ -645,7 +647,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - # User information, from scopes -> claims -@@ -668,7 +670,7 @@ class TestEndpoint(object): - - client_keyjar = KeyJar() - _jwks = self.server.keyjar.export_jwks() -- client_keyjar.import_jwks(_jwks, self.context.issuer) -+ client_keyjar = import_jwks(client_keyjar, _jwks, self.context.issuer) - _jwt = JWT(key_jar=client_keyjar, iss="client_1") - res = _jwt.unpack(id_token.value) - # Email didn't match -diff --git a/tests/test_server_09_authn_context.py b/tests/test_server_09_authn_context.py -index 4b9a72e..31b6ac7 100644 ---- a/tests/test_server_09_authn_context.py -+++ b/tests/test_server_09_authn_context.py -@@ -4,6 +4,7 @@ import os - import pytest - from cryptojwt.jwk.hmac import SYMKey - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.server import Server - from idpyoidc.server.authn_event import AuthnEvent - from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD -@@ -164,7 +165,7 @@ class TestAuthnBrokerEC: - "code id_token token", - ], - } -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - - self.server = server - -diff --git a/tests/test_server_12_session_life.py b/tests/test_server_12_session_life.py -index 50de8c8..64e7c13 100644 ---- a/tests/test_server_12_session_life.py -+++ b/tests/test_server_12_session_life.py -@@ -3,6 +3,7 @@ import os - import pytest - from cryptojwt.key_jar import init_key_jar - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest - from idpyoidc.message.oidc import RefreshAccessTokenRequest -@@ -202,7 +203,7 @@ KEYDEFS = [ - ISSUER = "https://example.com/" - - KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) --KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") -+KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) - RESPONSE_TYPES_SUPPORTED = [ - ["code"], - ["token"], -diff --git a/tests/test_server_16_endpoint.py b/tests/test_server_16_endpoint.py -index 5a3b59d..7c02399 100755 ---- a/tests/test_server_16_endpoint.py -+++ b/tests/test_server_16_endpoint.py -@@ -209,7 +209,7 @@ class TestEndpoint(object): - def test_do_response_placement_body(self): - self.endpoint.response_placement = "body" - info = self.endpoint.do_response(EXAMPLE_MSG) -- assert ("Content-type", "application/json; charset=utf-8") in info["http_headers"] -+ assert ("Content-type", "application/json") in info["http_headers"] - assert ( - info["response"] == '{"name": "Doe, Jane", "given_name": "Jane", "family_name": ' - '"Doe"}' -@@ -217,6 +217,7 @@ class TestEndpoint(object): - - def test_do_response_placement_url(self): - self.endpoint.response_placement = "url" -+ self.endpoint.response_format = "urlencoded" - info = self.endpoint.do_response(EXAMPLE_MSG, return_uri="https://example.org/cb") - assert ("Content-type", "application/x-www-form-urlencoded") in info["http_headers"] - assert ( -diff --git a/tests/test_server_16_endpoint_context.py b/tests/test_server_16_endpoint_context.py -index bf4b828..a462ce8 100644 ---- a/tests/test_server_16_endpoint_context.py -+++ b/tests/test_server_16_endpoint_context.py -@@ -1,17 +1,14 @@ - import copy - import os - --import pytest - from cryptojwt.key_jar import build_keyjar -+import pytest - --from idpyoidc import metadata -+from idpyoidc import alg_info - from idpyoidc.server import OPConfiguration - from idpyoidc.server import Server - from idpyoidc.server.endpoint import Endpoint --from idpyoidc.server.exception import OidcEndpointError - from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD --from idpyoidc.server.util import allow_refresh_token -- - from . import CRYPT_CONFIG - from . import SESSION_PARAMS - from . import full_path -@@ -28,9 +25,9 @@ class Endpoint_1(Endpoint): - name = "userinfo" - _supports = { - "claim_types_supported": ["normal", "aggregated", "distributed"], -- "userinfo_signing_alg_values_supported": metadata.get_signing_algs(), -- "userinfo_encryption_alg_values_supported": metadata.get_encryption_algs(), -- "userinfo_encryption_enc_values_supported": metadata.get_encryption_encs(), -+ "userinfo_signing_alg_values_supported": alg_info.get_signing_algs(), -+ "userinfo_encryption_alg_values_supported": alg_info.get_encryption_algs(), -+ "userinfo_encryption_enc_values_supported": alg_info.get_encryption_encs(), - "client_authn_method": ["bearer_header", "bearer_body"], - "encrypt_userinfo_supported": False, - } -diff --git a/tests/test_server_17_client_authn.py b/tests/test_server_17_client_authn.py -index b329644..aadaa72 100644 ---- a/tests/test_server_17_client_authn.py -+++ b/tests/test_server_17_client_authn.py -@@ -6,14 +6,16 @@ from unittest.mock import MagicMock - import pytest - from cryptojwt.jws.exception import NoSuitableSigningKeys - from cryptojwt.jwt import JWT --from cryptojwt.key_jar import KeyJar - from cryptojwt.key_jar import build_keyjar -+from cryptojwt.key_jar import KeyJar - from cryptojwt.utils import as_bytes - from cryptojwt.utils import as_unicode - - from idpyoidc.defaults import JWT_BEARER --from idpyoidc.server import Server -+from idpyoidc.key_import import import_jwks - from idpyoidc.server import do_endpoints -+from idpyoidc.server import Server -+from idpyoidc.server.client_authn import basic_authn - from idpyoidc.server.client_authn import BearerBody - from idpyoidc.server.client_authn import BearerHeader - from idpyoidc.server.client_authn import ClientSecretBasic -@@ -21,7 +23,6 @@ from idpyoidc.server.client_authn import ClientSecretJWT - from idpyoidc.server.client_authn import ClientSecretPost - from idpyoidc.server.client_authn import JWSAuthnMethod - from idpyoidc.server.client_authn import PrivateKeyJWT --from idpyoidc.server.client_authn import basic_authn - from idpyoidc.server.client_authn import verify_client - from idpyoidc.server.endpoint import Endpoint - from idpyoidc.server.exception import ClientAuthenticationError -@@ -49,7 +50,7 @@ class Endpoint_3(Endpoint): - name = "endpoint_3" - - def __init__( -- self, upstream_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs -+ self, upstream_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs - ): - Endpoint.__init__( - self, -@@ -127,6 +128,7 @@ def get_client_id_from_token(context, token, request=None): - - - class TestClientSecretBasic: -+ - @pytest.fixture(autouse=True) - def setup(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -162,6 +164,7 @@ class TestClientSecretBasic: - - - class TestClientSecretPost: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -185,6 +188,7 @@ class TestClientSecretPost: - - - class TestClientSecretJWT: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -194,7 +198,7 @@ class TestClientSecretJWT: - - def test_client_secret_jwt(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has at this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -212,6 +216,7 @@ class TestClientSecretJWT: - - - class TestPrivateKeyJWT: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -225,10 +230,10 @@ class TestPrivateKeyJWT: - # Own dynamic keys - client_keyjar = build_keyjar(KEYDEFS) - # The servers keys -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwks = client_keyjar.export_jwks() -- self.server.keyjar.import_jwks(_jwks, client_id) -+ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") - _jwt.with_jti = True -@@ -246,10 +251,10 @@ class TestPrivateKeyJWT: - # Own dynamic keys - client_keyjar = build_keyjar(KEYDEFS) - # The servers keys -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwks = client_keyjar.export_jwks() -- self.server.keyjar.import_jwks(_jwks, client_id) -+ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") - _jwt.with_jti = True -@@ -273,10 +278,10 @@ class TestPrivateKeyJWT: - # Own dynamic keys - client_keyjar = build_keyjar(KEYDEFS) - # The servers keys -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwks = client_keyjar.export_jwks() -- self.server.keyjar.import_jwks(_jwks, client_id) -+ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") - _jwt.with_jti = True -@@ -295,6 +300,7 @@ class TestPrivateKeyJWT: - - - class TestBearerHeader: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -318,6 +324,7 @@ class TestBearerHeader: - - - class TestBearerBody: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -341,6 +348,7 @@ class TestBearerBody: - - - class TestJWSAuthnMethod: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -352,7 +360,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_wrong_key(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # Fake symmetric key - client_keyjar.add_symmetric("", "client_secret:client_secret", ["sig"]) - -@@ -366,7 +374,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_iss(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar,KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -381,7 +389,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_token_endpoint(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -401,7 +409,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_not_me(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has at this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -419,7 +427,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_userinfo_endpoint(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -465,6 +473,7 @@ def test_basic_auth_wrong_token(): - - - class TestVerify: -+ - @pytest.fixture(autouse=True) - def create_method(self): - self.server = Server(conf=CONF, keyjar=KEYJAR) -@@ -520,7 +529,7 @@ class TestVerify: - - def test_verify_client_jws_authn_method(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -582,6 +591,7 @@ class TestVerify: - - - class TestVerify2: -+ - @pytest.fixture(autouse=True) - def create_method(self): - self.server = Server(conf=CONF, keyjar=KEYJAR) -@@ -591,7 +601,7 @@ class TestVerify2: - - def test_verify_client_jws_authn_method(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -diff --git a/tests/test_server_20a_server.py b/tests/test_server_20a_server.py -index 8413b01..32968ed 100755 ---- a/tests/test_server_20a_server.py -+++ b/tests/test_server_20a_server.py -@@ -122,7 +122,7 @@ def test_capabilities_default(): - "id_token", - "code id_token", - } -- assert server.context.provider_info["request_uri_parameter_supported"] is True -+ assert server.context.provider_info["request_uri_parameter_supported"] is False - assert server.context.get_preference("jwks_uri") == "https://127.0.0.1:443/static/jwks.json" - - -@@ -137,10 +137,7 @@ def test_capabilities_subset2(): - _cnf = deepcopy(CONF) - _cnf["response_types_supported"] = ["code", "id_token"] - server = Server(_cnf) -- assert set(server.context.provider_info["response_types_supported"]) == { -- "code", -- "id_token", -- } -+ assert set(server.context.provider_info["response_types_supported"]) == {"code", "id_token"} - - - def test_capabilities_bool(): -diff --git a/tests/test_server_20d_client_authn.py b/tests/test_server_20d_client_authn.py -index d45e342..152cb52 100755 ---- a/tests/test_server_20d_client_authn.py -+++ b/tests/test_server_20d_client_authn.py -@@ -4,13 +4,17 @@ from unittest.mock import MagicMock - import pytest - from cryptojwt.jws.exception import NoSuitableSigningKeys - from cryptojwt.jwt import JWT --from cryptojwt.key_jar import KeyJar - from cryptojwt.key_jar import build_keyjar -+from cryptojwt.key_jar import KeyJar - from cryptojwt.utils import as_bytes - from cryptojwt.utils import as_unicode - - from idpyoidc.defaults import JWT_BEARER -+from idpyoidc.key_import import add_keys -+from idpyoidc.key_import import add_symmetric -+from idpyoidc.key_import import import_jwks - from idpyoidc.server import Server -+from idpyoidc.server.client_authn import basic_authn - from idpyoidc.server.client_authn import BearerBody - from idpyoidc.server.client_authn import BearerHeader - from idpyoidc.server.client_authn import ClientSecretBasic -@@ -18,7 +22,6 @@ from idpyoidc.server.client_authn import ClientSecretJWT - from idpyoidc.server.client_authn import ClientSecretPost - from idpyoidc.server.client_authn import JWSAuthnMethod - from idpyoidc.server.client_authn import PrivateKeyJWT --from idpyoidc.server.client_authn import basic_authn - from idpyoidc.server.client_authn import verify_client - from idpyoidc.server.exception import ClientAuthenticationError - from idpyoidc.server.exception import InvalidToken -@@ -88,6 +91,7 @@ def get_client_id_from_token(context, token, request=None): - - - class TestClientSecretBasic: -+ - @pytest.fixture(autouse=True) - def setup(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -122,6 +126,7 @@ class TestClientSecretBasic: - - - class TestClientSecretPost: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -145,6 +150,7 @@ class TestClientSecretPost: - - - class TestClientSecretJWT: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -154,9 +160,10 @@ class TestClientSecretJWT: - - def test_client_secret_jwt(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -- # The only own key the client has a this point -- client_keyjar.add_symmetric("", client_secret, ["sig"]) -+ # The only own key the client has at this point -+ client_keyjar = add_symmetric(client_keyjar, client_secret, "") -+ # The issuers keys -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="HS256") - _jwt.with_jti = True -@@ -164,6 +171,7 @@ class TestClientSecretJWT: - - request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER} - -+ self.context.keyjar = add_keys(self.context.keyjar, client_keyjar.get(key_use="sig", key_type="oct"), client_id) - assert self.method.is_usable(request=request) - authn_info = self.method.verify(request=request) - -@@ -172,6 +180,7 @@ class TestClientSecretJWT: - - - class TestPrivateKeyJWT: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -184,10 +193,10 @@ class TestPrivateKeyJWT: - # Own dynamic keys - client_keyjar = build_keyjar(KEYDEFS) - # The servers keys -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwks = client_keyjar.export_jwks() -- self.server.keyjar.import_jwks(_jwks, client_id) -+ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") - _jwt.with_jti = True -@@ -205,10 +214,10 @@ class TestPrivateKeyJWT: - # Own dynamic keys - client_keyjar = build_keyjar(KEYDEFS) - # The servers keys -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwks = client_keyjar.export_jwks() -- self.server.keyjar.import_jwks(_jwks, client_id) -+ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") - _jwt.with_jti = True -@@ -232,10 +241,10 @@ class TestPrivateKeyJWT: - # Own dynamic keys - client_keyjar = build_keyjar(KEYDEFS) - # The servers keys -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - - _jwks = client_keyjar.export_jwks() -- self.server.keyjar.import_jwks(_jwks, client_id) -+ self.server.keyjar = import_jwks(self.server.keyjar, _jwks, client_id) - - _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") - _jwt.with_jti = True -@@ -254,6 +263,7 @@ class TestPrivateKeyJWT: - - - class TestBearerHeader: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -276,6 +286,7 @@ class TestBearerHeader: - - - class TestBearerBody: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -298,6 +309,7 @@ class TestBearerBody: - - - class TestJWSAuthnMethod: -+ - @pytest.fixture(autouse=True) - def create_method(self): - server = Server(conf=CONF, keyjar=KEYJAR) -@@ -308,7 +320,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_wrong_key(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # Fake symmetric key - client_keyjar.add_symmetric("", "client_secret:client_secret", ["sig"]) - -@@ -322,7 +334,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_iss(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -337,7 +349,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_token_endpoint(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -357,7 +369,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_not_me(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has at this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -375,7 +387,7 @@ class TestJWSAuthnMethod: - - def test_jws_authn_method_aud_userinfo_endpoint(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -421,6 +433,7 @@ def test_basic_auth_wrong_token(): - - - class TestVerify: -+ - @pytest.fixture(autouse=True) - def create_method(self): - self.server = Server(conf=CONF, keyjar=KEYJAR) -@@ -475,7 +488,7 @@ class TestVerify: - - def test_verify_client_jws_authn_method(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -@@ -546,6 +559,7 @@ class TestVerify: - - - class TestVerify2: -+ - @pytest.fixture(autouse=True) - def create_method(self): - self.server = Server(conf=CONF, keyjar=KEYJAR) -@@ -554,7 +568,7 @@ class TestVerify2: - - def test_verify_client_jws_authn_method(self): - client_keyjar = KeyJar() -- client_keyjar.import_jwks(KEYJAR.export_jwks(private=True), CONF["issuer"]) -+ client_keyjar = import_jwks(client_keyjar, KEYJAR.export_jwks(private=True), CONF["issuer"]) - # The only own key the client has a this point - client_keyjar.add_symmetric("", client_secret, ["sig"]) - -diff --git a/tests/test_server_20e_jwt_token.py b/tests/test_server_20e_jwt_token.py -index d824bd1..ef295bf 100644 ---- a/tests/test_server_20e_jwt_token.py -+++ b/tests/test_server_20e_jwt_token.py -@@ -4,6 +4,7 @@ import pytest - from cryptojwt.jwt import JWT - from cryptojwt.key_jar import init_key_jar - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest - from idpyoidc.server import Server -@@ -31,7 +32,7 @@ KEYDEFS = [ - ISSUER = "https://example.com/" - - KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) --KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") -+KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) - - RESPONSE_TYPES_SUPPORTED = [ - ["code"], -@@ -98,6 +99,7 @@ def full_path(local_file): - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -@@ -297,6 +299,7 @@ class TestEndpoint(object): - - - class TestEndpointWebID(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - _scope2claims = SCOPE2CLAIMS.copy() -diff --git a/tests/test_server_22_oidc_provider_config_endpoint.py b/tests/test_server_22_oidc_provider_config_endpoint.py -index 7000d72..fe1f076 100755 ---- a/tests/test_server_22_oidc_provider_config_endpoint.py -+++ b/tests/test_server_22_oidc_provider_config_endpoint.py -@@ -19,12 +19,8 @@ KEYDEFS = [ - - RESPONSE_TYPES_SUPPORTED = [ - ["code"], -- ["token"], - ["id_token"], -- ["code", "token"], - ["code", "id_token"], -- ["id_token", "token"], -- ["code", "token", "id_token"], - ["none"], - ] - -@@ -92,16 +88,16 @@ class TestProviderConfigEndpoint(object): - assert _msg["token_endpoint"] == "https://example.com/token" - assert _msg["jwks_uri"] == "https://example.com/static/jwks.json" - assert "claims_supported" not in _msg # No default for this -- assert ("Content-type", "application/json; charset=utf-8") in msg["http_headers"] -+ assert ("Content-type", "application/json") in msg["http_headers"] - -- def test_scopes_supported(self, conf): -- scopes_supported = ["openid", "random", "profile"] -- conf["scopes_supported"] = scopes_supported -- -- server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) -- endpoint = server.get_endpoint("provider_config") -- args = endpoint.process_request() -- msg = endpoint.do_response(args["response_args"]) -- assert isinstance(msg, dict) -- _msg = json.loads(msg["response"]) -- assert set(_msg["scopes_supported"]) == set(scopes_supported) -+ # def test_scopes_supported(self, conf): -+ # scopes_supported = ["openid", "random", "profile"] -+ # conf["scopes_supported"] = scopes_supported -+ # -+ # server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) -+ # endpoint = server.get_endpoint("provider_config") -+ # args = endpoint.process_request() -+ # msg = endpoint.do_response(args["response_args"]) -+ # assert isinstance(msg, dict) -+ # _msg = json.loads(msg["response"]) -+ # assert set(_msg["scopes_supported"]) == set(scopes_supported) -diff --git a/tests/test_server_24_oauth2_authorization_endpoint.py b/tests/test_server_24_oauth2_authorization_endpoint.py -index 925424c..d0a010c 100755 ---- a/tests/test_server_24_oauth2_authorization_endpoint.py -+++ b/tests/test_server_24_oauth2_authorization_endpoint.py -@@ -5,16 +5,17 @@ from http.cookies import SimpleCookie - from urllib.parse import parse_qs - from urllib.parse import urlparse - --from cryptojwt.jws.jws import factory - import pytest - import yaml - from cryptojwt import KeyJar -+from cryptojwt.jws.jws import factory - 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.key_import import store_under_other_id - from idpyoidc.message.oauth2 import AuthorizationErrorResponse - from idpyoidc.message.oauth2 import AuthorizationRequest - from idpyoidc.message.oauth2 import AuthorizationResponse -@@ -31,8 +32,8 @@ 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.authorization import FORM_POST - from idpyoidc.server.oauth2.authorization import get_uri - from idpyoidc.server.oauth2.authorization import inputs - from idpyoidc.server.oauth2.authorization import join_query -@@ -84,6 +85,7 @@ USERINFO_db = json.loads(open(full_path("users.json")).read()) - - - class SimpleCookieDealer(object): -+ - def __init__(self, name=""): - self.name = name - -@@ -159,6 +161,7 @@ clients: - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -@@ -265,7 +268,7 @@ class TestEndpoint(object): - context = server.context - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = _clients["clients"] -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - self.context = context - self.endpoint = server.get_endpoint("authorization") - self.session_manager = context.session_manager -@@ -370,7 +373,8 @@ class TestEndpoint(object): - ) - def test_verify_uri_localhost_ipv4_native_client(self, client_redirect_uri, redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], -+ "application_type": APPLICATION_TYPE_NATIVE} - request = {"redirect_uri": redirect_uri} - - verify_uri(_context, request, "redirect_uri", "client_id") -@@ -381,15 +385,20 @@ class TestEndpoint(object): - ("http://[::1]:9999/auth_cb", "http://[::1]:3456/auth_cb"), - ("http://[::1]/auth_cb", "http://[::1]/auth_cb"), - ("http://[::1]/auth_cb", "http://[::1]:3456/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), - ] - ) - def test_verify_uri_localhost_ipv6_native_client(self, client_redirect_uri, redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], -+ "application_type": APPLICATION_TYPE_NATIVE} - request = {"redirect_uri": redirect_uri} - - verify_uri(_context, request, "redirect_uri", "client_id") -@@ -403,10 +412,11 @@ class TestEndpoint(object): - ) - def test_verify_uri_literal_localhost_native_client(self, client_redirect_uri, redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], -+ "application_type": APPLICATION_TYPE_NATIVE} - request = {"redirect_uri": redirect_uri} - with pytest.raises(RedirectURIError): -- verify_uri(_context, request, "redirect_uri", "client_id") -+ verify_uri(_context, request, "redirect_uri", "client_id") - - @pytest.mark.parametrize( - "client_redirect_uri, redirect_uri", [ -@@ -417,71 +427,85 @@ class TestEndpoint(object): - ) - def test_verify_uri_localhost_ipv4_web_client(self, client_redirect_uri, redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_WEB} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], -+ "application_type": APPLICATION_TYPE_WEB} - request = {"redirect_uri": redirect_uri} - with pytest.raises(RedirectURIError): -- verify_uri(_context, request, "redirect_uri", "client_id") -+ verify_uri(_context, request, "redirect_uri", "client_id") - - @pytest.mark.parametrize( - "client_redirect_uri, redirect_uri", [ - ("http://[::1]:9999/auth_cb", "http://[::1]/auth_cb"), - ("http://[::1]:9999/auth_cb", "http://[::1]:3456/auth_cb"), - ("http://[::1]/auth_cb", "http://[::1]:3456/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), - ] - ) - def test_verify_uri_localhost_ipv6_web_client(self, client_redirect_uri, redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_WEB} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})], -+ "application_type": APPLICATION_TYPE_WEB} - request = {"redirect_uri": redirect_uri} - with pytest.raises(RedirectURIError): -- verify_uri(_context, request, "redirect_uri", "client_id") -+ verify_uri(_context, request, "redirect_uri", "client_id") - - @pytest.mark.parametrize( - "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ -- ("http://127.0.0.1:9999/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:9999/auth_cb?foo=bar"), -- ("http://127.0.0.1:9999/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), -- ("http://127.0.0.1/auth_cb", {"foo":["bar"]}, "http://127.0.0.1/auth_cb?foo=bar"), -- ("http://127.0.0.1/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), -+ ("http://127.0.0.1:9999/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1:9999/auth_cb?foo=bar"), -+ ("http://127.0.0.1:9999/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), -+ ("http://127.0.0.1/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1/auth_cb?foo=bar"), -+ ("http://127.0.0.1/auth_cb", {"foo": ["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), - ] - ) -- def test_verify_uri_qp_localhost_ipv4_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): -+ def test_verify_uri_qp_localhost_ipv4_native_client(self, client_redirect_uri, client_redirect_uri_qp, -+ redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)],"application_type": APPLICATION_TYPE_NATIVE} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], -+ "application_type": APPLICATION_TYPE_NATIVE} - request = {"redirect_uri": redirect_uri} - - verify_uri(_context, request, "redirect_uri", "client_id") - - @pytest.mark.parametrize( - "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ -- ("http://[::1]:9999/auth_cb", {"foo":["bar"]}, "http://[::1]:9999/auth_cb?foo=bar"), -- ("http://[::1]:9999/auth_cb", {"foo":["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), -- ("http://[::1]/auth_cb", {"foo":["bar"]}, "http://[::1]/auth_cb?foo=bar"), -- ("http://[::1]/auth_cb", {"foo":["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), -- ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), -+ ("http://[::1]:9999/auth_cb", {"foo": ["bar"]}, "http://[::1]:9999/auth_cb?foo=bar"), -+ ("http://[::1]:9999/auth_cb", {"foo": ["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), -+ ("http://[::1]/auth_cb", {"foo": ["bar"]}, "http://[::1]/auth_cb?foo=bar"), -+ ("http://[::1]/auth_cb", {"foo": ["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo": ["bar"]}, -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo": ["bar"]}, -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo": ["bar"]}, -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), -+ ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo": ["bar"]}, -+ "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), - ] - ) -- def test_verify_uri_qp_localhost_ipv6_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): -+ def test_verify_uri_qp_localhost_ipv6_native_client(self, client_redirect_uri, client_redirect_uri_qp, -+ redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], "application_type": APPLICATION_TYPE_NATIVE} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], -+ "application_type": APPLICATION_TYPE_NATIVE} - request = {"redirect_uri": redirect_uri} - - verify_uri(_context, request, "redirect_uri", "client_id") - - @pytest.mark.parametrize( - "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ -- ("https://rp.example.com:9999/auth_cb", {"foo":["bar"]}, "http://rp.example.com/auth_cb?foo=bar"), -- ("https://rp.example.com/auth_cb", {"foo":["bar"]}, "http://rp.example.com:9999/auth_cb?foo=bar"), -+ ("https://rp.example.com:9999/auth_cb", {"foo": ["bar"]}, "http://rp.example.com/auth_cb?foo=bar"), -+ ("https://rp.example.com/auth_cb", {"foo": ["bar"]}, "http://rp.example.com:9999/auth_cb?foo=bar"), - ] - ) -- def test_verify_uri_qp_match_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): -+ def test_verify_uri_qp_match_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): - _context = self.endpoint.upstream_get("context") -- _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], "application_type": APPLICATION_TYPE_NATIVE} -+ _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], -+ "application_type": APPLICATION_TYPE_NATIVE} - - request = {"redirect_uri": redirect_uri} - -@@ -922,7 +946,6 @@ class TestEndpoint(object): - _payload = _jws.jwt.payload() - assert 'aud' in _payload - -- - # def test_audience(self): - # request = AuthorizationRequest( - # client_id="client_id", -@@ -948,6 +971,7 @@ class TestEndpoint(object): - # res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None) - # assert set(res.keys()) == {"session_id", "identity", "user"} - -+ - def test_inputs(): - elems = inputs(dict(foo="bar", home="stead")) - test_elems = ( -diff --git a/tests/test_server_24_oauth2_authorization_endpoint_jar.py b/tests/test_server_24_oauth2_authorization_endpoint_jar.py -index f788c7e..e57360e 100755 ---- a/tests/test_server_24_oauth2_authorization_endpoint_jar.py -+++ b/tests/test_server_24_oauth2_authorization_endpoint_jar.py -@@ -10,6 +10,7 @@ from cryptojwt import JWT - from cryptojwt import KeyJar - from cryptojwt.jwt import utc_time_sans_frac - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import AuthorizationRequest - from idpyoidc.message.oauth2 import JWTSecuredAuthorizationRequest - from idpyoidc.server import Server -@@ -28,15 +29,6 @@ KEYDEFS = [ - - 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", -@@ -58,6 +50,7 @@ USERINFO_db = json.loads(open(full_path("users.json")).read()) - - - class SimpleCookieDealer(object): -+ - def __init__(self, name=""): - self.name = name - -@@ -133,24 +126,29 @@ clients: - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { - "issuer": "https://example.com/", - "password": "mycket hemligt zebra", -- "verify_ssl": False, -- "grant_types_supported": [ -- "authorization_code", -- "implicit", -- "urn:ietf:params:oauth:grant-type:jwt-bearer", -- "refresh_token", -- ], -+ "httpc_params": { -+ "verify": False -+ }, -+ "preference": { -+ "grant_types_supported": [ -+ "authorization_code", -+ "urn:ietf:params:oauth:grant-type:jwt-bearer", -+ "refresh_token", -+ ], -+ "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, -+ "request_object_signing_alg_values_supported": ["HS256"] -+ }, - "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, -- "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, - "request_cls": JWTSecuredAuthorizationRequest, - "endpoint": { - "authorization": { -@@ -190,7 +188,7 @@ class TestEndpoint(object): - context = server.context - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = _clients["clients"] -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - self.endpoint = server.get_endpoint("authorization") - self.session_manager = context.session_manager - self.user_id = "diana" -diff --git a/tests/test_server_24_oauth2_resource_indicators.py b/tests/test_server_24_oauth2_resource_indicators.py -index 14e6a03..bb91a46 100644 ---- a/tests/test_server_24_oauth2_resource_indicators.py -+++ b/tests/test_server_24_oauth2_resource_indicators.py -@@ -2,23 +2,14 @@ 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 import KeyJar --from cryptojwt.jwt import JWT - from cryptojwt.jwt import utc_time_sans_frac --from cryptojwt.key_jar import init_key_jar --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 -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import AuthorizationRequest --from idpyoidc.message.oauth2 import AuthorizationResponse - from idpyoidc.message.oauth2 import TokenErrorResponse - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.server import Server -@@ -26,21 +17,10 @@ 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.authorization import get_uri --from idpyoidc.server.oauth2.authorization import inputs --from idpyoidc.server.oauth2.authorization import join_query - from idpyoidc.server.oauth2.authorization import ( - validate_resource_indicators_policy as validate_authorization_resource_indicators_policy, - ) --from idpyoidc.server.oauth2.authorization import verify_uri - from idpyoidc.server.oauth2.token import Token - from idpyoidc.server.oauth2.token_helper import ( - validate_resource_indicators_policy as validate_token_resource_indicators_policy, -@@ -104,6 +84,7 @@ USERINFO_db = json.loads(open(full_path("users.json")).read()) - - - class SimpleCookieDealer(object): -+ - def __init__(self, name=""): - self.name = name - -@@ -415,6 +396,7 @@ RESOURCE_INDICATORS_ENABLED = { - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=False) - def create_endpoint_ri_disabled(self): - conf = RESOURCE_INDICATORS_DISABLED -@@ -423,9 +405,7 @@ class TestEndpoint(object): - endpoint_context = server.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"] -- ) -+ endpoint_context.keyjar = store_under_other_id(endpoint_context.keyjar, "", conf["issuer"], True) - self.endpoint_context = endpoint_context - self.endpoint = server.get_endpoint("authorization") - self.token_endpoint = server.get_endpoint("token") -@@ -446,9 +426,8 @@ class TestEndpoint(object): - endpoint_context = server.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"] -- ) -+ endpoint_context.keyjar = store_under_other_id(endpoint_context.keyjar, "", -+ conf["issuer"], True) - self.endpoint_context = endpoint_context - self.endpoint = server.get_endpoint("authorization") - self.token_endpoint = server.get_endpoint("token") -@@ -529,7 +508,7 @@ class TestEndpoint(object): - assert msg[key] == request[key] - - def test_authorization_code_req_no_resource_indicators_disabled( -- self, create_endpoint_ri_disabled -+ self, create_endpoint_ri_disabled - ): - """ - Test successful authorization request when resource indicators is disabled. -diff --git a/tests/test_server_24_oauth2_token_endpoint.py b/tests/test_server_24_oauth2_token_endpoint.py -index 4d43d30..e78850a 100644 ---- a/tests/test_server_24_oauth2_token_endpoint.py -+++ b/tests/test_server_24_oauth2_token_endpoint.py -@@ -1,14 +1,15 @@ - import json - import os - -+import pytest - from cryptojwt import JWT - from cryptojwt import KeyJar - from cryptojwt.jws.jws import factory - from cryptojwt.key_jar import build_keyjar --import pytest - - from idpyoidc.context import OidcContext - from idpyoidc.defaults import JWT_BEARER -+from idpyoidc.key_import import import_jwks - from idpyoidc.message import Message - from idpyoidc.message import REQUIRED_LIST_OF_STRINGS - from idpyoidc.message import SINGLE_REQUIRED_INT -@@ -26,6 +27,7 @@ from idpyoidc.server.authz import AuthzHandling - from idpyoidc.server.client_authn import verify_client - from idpyoidc.server.configure import ASConfiguration - from idpyoidc.server.exception import InvalidToken -+from idpyoidc.server.exception import UnAuthorizedClient - from idpyoidc.server.oauth2.authorization import Authorization - from idpyoidc.server.oauth2.token import Token - from idpyoidc.server.token import handler -@@ -172,6 +174,7 @@ def conf(): - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self, conf): - server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) -@@ -184,7 +187,7 @@ class TestEndpoint(object): - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], - } -- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") -+ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") - self.session_manager = context.session_manager - self.token_endpoint = server.get_endpoint("token") - self.user_id = "diana" -@@ -342,7 +345,7 @@ class TestEndpoint(object): - _resp = self.token_endpoint.process_request(request=_req) - - # 2nd time used -- with pytest.raises(InvalidToken): -+ with pytest.raises((InvalidToken, UnAuthorizedClient)): - self.token_endpoint.parse_request(_token_request) - - def test_do_refresh_access_token(self): -@@ -855,8 +858,8 @@ CONTEXT.cwd = BASEDIR - CONTEXT.issuer = "https://op.example.com" - CONTEXT.cdb = {"client_1": {}} - KEYJAR = KeyJar() --KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "client_1") --KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "") -+KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "client_1") -+KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "") - - - def upstream_get(what, *args): -@@ -919,6 +922,7 @@ def test_jwttoken_2(): - - - class TestClientCredentialsFlow(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self, conf): - server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) -@@ -956,6 +960,7 @@ class TestClientCredentialsFlow(object): - - - class TestResourceOwnerPasswordCredentialsFlow(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self, conf): - conf["authentication"] = { -diff --git a/tests/test_server_24_oauth2_token_endpoint_def_conf.py b/tests/test_server_24_oauth2_token_endpoint_def_conf.py -index 9508002..2b181a2 100644 ---- a/tests/test_server_24_oauth2_token_endpoint_def_conf.py -+++ b/tests/test_server_24_oauth2_token_endpoint_def_conf.py -@@ -8,10 +8,11 @@ from cryptojwt.key_jar import build_keyjar - - from idpyoidc.context import OidcContext - from idpyoidc.defaults import JWT_BEARER -+from idpyoidc.key_import import import_jwks -+from idpyoidc.message import Message - from idpyoidc.message import REQUIRED_LIST_OF_STRINGS - from idpyoidc.message import SINGLE_REQUIRED_INT - from idpyoidc.message import SINGLE_REQUIRED_STRING --from idpyoidc.message import Message - from idpyoidc.message.oauth2 import AccessTokenRequest - from idpyoidc.message.oauth2 import AuthorizationRequest - from idpyoidc.message.oauth2 import CCAccessTokenRequest -@@ -23,6 +24,7 @@ from idpyoidc.server import Server - from idpyoidc.server.authn_event import create_authn_event - from idpyoidc.server.configure import ASConfiguration - from idpyoidc.server.exception import InvalidToken -+from idpyoidc.server.exception import UnAuthorizedClient - from idpyoidc.server.token import handler - from idpyoidc.time_util import utc_time_sans_frac - from tests import CRYPT_CONFIG -@@ -64,6 +66,7 @@ def full_path(local_file): - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -@@ -83,7 +86,7 @@ class TestEndpoint(object): - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], - } -- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") -+ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") - self.session_manager = context.session_manager - self.token_endpoint = server.get_endpoint("token") - self.user_id = "diana" -@@ -241,7 +244,7 @@ class TestEndpoint(object): - _resp = self.token_endpoint.process_request(request=_req) - - # 2nd time used -- with pytest.raises(InvalidToken): -+ with pytest.raises((InvalidToken, UnAuthorizedClient)): - self.token_endpoint.parse_request(_token_request) - - def test_do_refresh_access_token(self): -@@ -736,8 +739,8 @@ CONTEXT.cwd = BASEDIR - CONTEXT.issuer = "https://op.example.com" - CONTEXT.cdb = {"client_1": {}} - KEYJAR = KeyJar() --KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "client_1") --KEYJAR.import_jwks(CLIENT_KEYJAR.export_jwks(private=True), "") -+KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "client_1") -+KEYJAR = import_jwks(KEYJAR, CLIENT_KEYJAR.export_jwks(private=True), "") - - - def upstream_get(what, *args): -@@ -800,6 +803,7 @@ def test_jwttoken_2(): - - - class TestClientCredentialsFlow(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -@@ -845,6 +849,7 @@ class TestClientCredentialsFlow(object): - - - class TestResourceOwnerPasswordCredentialsFlow(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -diff --git a/tests/test_server_24_oidc_authorization_endpoint.py b/tests/test_server_24_oidc_authorization_endpoint.py -index 7facfcd..73f6524 100755 ---- a/tests/test_server_24_oidc_authorization_endpoint.py -+++ b/tests/test_server_24_oidc_authorization_endpoint.py -@@ -15,6 +15,7 @@ from cryptojwt.utils import b64e - - from idpyoidc.exception import ParameterError - from idpyoidc.exception import URIError -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import AuthorizationErrorResponse - from idpyoidc.message.oauth2 import ResponseMessage - from idpyoidc.message.oidc import AuthorizationRequest -@@ -76,6 +77,8 @@ CAPABILITIES = { - "urn:ietf:params:oauth:grant-type:jwt-bearer", - "refresh_token", - ], -+ "request_uri_parameter_supported": True, -+ "request_object_signing_alg_values_supported": ["HS256"] - } - - CLAIMS = {"id_token": {"given_name": {"essential": True}, "nickname": None}} -@@ -157,7 +160,7 @@ class TestEndpoint(object): - "issuer": "https://example.com/", - "password": "mycket hemligt zebra", - "verify_ssl": False, -- "capabilities": CAPABILITIES, -+ "preference": CAPABILITIES, - "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, - "token_handler_args": { - "jwks_file": "private/token_jwks.json", -@@ -287,7 +290,7 @@ class TestEndpoint(object): - - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = _clients["oidc_clients"] -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - self.context = context - self.endpoint = server.get_endpoint("authorization") - self.session_manager = context.session_manager -@@ -1191,7 +1194,7 @@ class TestACR(object): - - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = _clients["oidc_clients"] -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - self.endpoint = server.get_endpoint("authorization") - self.session_manager = context.session_manager - self.user_id = "diana" -diff --git a/tests/test_server_26_oidc_userinfo_endpoint.py b/tests/test_server_26_oidc_userinfo_endpoint.py -index 1d76e45..3cbb275 100755 ---- a/tests/test_server_26_oidc_userinfo_endpoint.py -+++ b/tests/test_server_26_oidc_userinfo_endpoint.py -@@ -551,8 +551,9 @@ class TestEndpoint(object): - - monkeypatch.setattr("idpyoidc.server.token.utc_time_sans_frac", mock) - -- with pytest.raises(BearerTokenAuthenticationError): -- self.endpoint.parse_request({}, http_info=http_info) -+ res = self.endpoint.parse_request({}, http_info=http_info) -+ assert "error" in res -+ assert res["error"] == "invalid_token" - - def test_userinfo_claims(self): - _acr = "https://refeds.org/profile/mfa" -diff --git a/tests/test_server_30_oidc_end_session.py b/tests/test_server_30_oidc_end_session.py -index 95255a7..abbeb66 100644 ---- a/tests/test_server_30_oidc_end_session.py -+++ b/tests/test_server_30_oidc_end_session.py -@@ -4,11 +4,13 @@ import os - from urllib.parse import parse_qs - from urllib.parse import urlparse - -+from cryptojwt.key_jar import build_keyjar - import pytest - import responses --from cryptojwt.key_jar import build_keyjar - -+from idpyoidc import alg_info - from idpyoidc.exception import InvalidRequest -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message import Message - from idpyoidc.message.oidc import AuthorizationRequest - from idpyoidc.message.oidc import verified_claim_name -@@ -43,7 +45,7 @@ KEYDEFS = [ - ] - - KEYJAR = build_keyjar(KEYDEFS) --KEYJAR.import_jwks(KEYJAR.export_jwks(private=True), ISS) -+KEYJAR = store_under_other_id(KEYJAR, "", ISS, True) - - RESPONSE_TYPES_SUPPORTED = [["code"], ["id_token"], ["code", "id_token"]] - -@@ -67,6 +69,7 @@ PREFRERENCES = { - "claims_parameter_supported": True, - "request_parameter_supported": True, - "request_uri_parameter_supported": True, -+ "id_token_signing_alg_values_supported": alg_info.get_signing_algs() - } - - AUTH_REQ = AuthorizationRequest( -diff --git a/tests/test_server_31_oauth2_introspection.py b/tests/test_server_31_oauth2_introspection.py -index bf28347..e5b597b 100644 ---- a/tests/test_server_31_oauth2_introspection.py -+++ b/tests/test_server_31_oauth2_introspection.py -@@ -8,6 +8,7 @@ from cryptojwt import as_unicode - from cryptojwt.key_jar import build_keyjar - from cryptojwt.utils import as_bytes - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import TokenIntrospectionRequest - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -207,9 +208,7 @@ class TestEndpoint: - }, - "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], - } -- server.keyjar.import_jwks_as_json( -- server.keyjar.export_jwks_as_json(private=True), context.issuer -- ) -+ server.keyjar = store_under_other_id(server.keyjar, "", context.issuer, True) - self.introspection_endpoint = server.get_endpoint("introspection") - self.token_endpoint = server.get_endpoint("token") - self.session_manager = context.session_manager -@@ -325,7 +324,7 @@ class TestEndpoint: - assert isinstance(msg_info, dict) - assert set(msg_info.keys()) == {"response", "http_headers"} - assert msg_info["http_headers"] == [ -- ("Content-type", "application/json; charset=utf-8"), -+ ("Content-type", "application/json"), - ("Pragma", "no-cache"), - ("Cache-Control", "no-store"), - ] -diff --git a/tests/test_server_32_oidc_read_registration.py b/tests/test_server_32_oidc_read_registration.py -index e09bc5c..2dea641 100644 ---- a/tests/test_server_32_oidc_read_registration.py -+++ b/tests/test_server_32_oidc_read_registration.py -@@ -160,4 +160,4 @@ class TestEndpoint(object): - - _endp_response = self.registration_api_endpoint.do_response(_info) - assert set(_endp_response.keys()) == {"response", "http_headers"} -- assert ("Content-type", "application/json; charset=utf-8") in _endp_response["http_headers"] -+ assert ("Content-type", "application/json") in _endp_response["http_headers"] -diff --git a/tests/test_server_33_oauth2_pkce.py b/tests/test_server_33_oauth2_pkce.py -index 60cf753..4a08f50 100644 ---- a/tests/test_server_33_oauth2_pkce.py -+++ b/tests/test_server_33_oauth2_pkce.py -@@ -7,6 +7,7 @@ import string - import pytest - import yaml - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message import Message - from idpyoidc.message.oauth2 import AuthorizationErrorResponse - from idpyoidc.message.oidc import AccessTokenRequest -@@ -229,7 +230,7 @@ def create_server(config): - context = server.context - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = _clients["oidc_clients"] -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), config["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", config["issuer"], True) - return server - - -diff --git a/tests/test_server_34_oidc_sso.py b/tests/test_server_34_oidc_sso.py -index 4090b51..61fb38d 100755 ---- a/tests/test_server_34_oidc_sso.py -+++ b/tests/test_server_34_oidc_sso.py -@@ -6,6 +6,7 @@ import pytest - import yaml - from cryptojwt import KeyJar - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oidc import AuthorizationRequest - from idpyoidc.server import Server - from idpyoidc.server.configure import OPConfiguration -@@ -199,7 +200,7 @@ class TestUserAuthn(object): - context = server.context - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = _clients["oidc_clients"] -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - self.endpoint = server.get_endpoint("authorization") - self.context = context - self.rp_keyjar = KeyJar() -diff --git a/tests/test_server_35_oidc_token_endpoint.py b/tests/test_server_35_oidc_token_endpoint.py -index d6af31e..c2371f3 100755 ---- a/tests/test_server_35_oidc_token_endpoint.py -+++ b/tests/test_server_35_oidc_token_endpoint.py -@@ -6,6 +6,9 @@ import pytest - from cryptojwt import JWT - from cryptojwt.key_jar import build_keyjar - -+from idpyoidc.key_import import import_jwks -+from idpyoidc.server.exception import UnAuthorizedClient -+ - from idpyoidc.defaults import JWT_BEARER - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -212,7 +215,7 @@ class TestEndpoint(_TestEndpoint): - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], - } -- self.server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") -+ self.server.keyjar = import_jwks(self.server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") - context.userinfo = USERINFO - self.session_manager = context.session_manager - self.token_endpoint = self.server.get_endpoint("token") -@@ -350,7 +353,7 @@ class TestEndpoint(_TestEndpoint): - _resp = self.token_endpoint.process_request(request=_req) - - # 2nd time used -- with pytest.raises(InvalidToken): -+ with pytest.raises((InvalidToken, UnAuthorizedClient)): - self.token_endpoint.parse_request(_token_request) - - def test_do_refresh_access_token(self): -@@ -1029,7 +1032,7 @@ class TestOldTokens(object): - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], - } -- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") -+ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") - self.session_manager = context.session_manager - self.token_endpoint = server.get_endpoint("token") - self.user_id = "diana" -diff --git a/tests/test_server_35_oidc_token_endpoint_def_conf.py b/tests/test_server_35_oidc_token_endpoint_def_conf.py -index f08fefa..74d4e87 100755 ---- a/tests/test_server_35_oidc_token_endpoint_def_conf.py -+++ b/tests/test_server_35_oidc_token_endpoint_def_conf.py -@@ -4,6 +4,9 @@ import pytest - from cryptojwt import JWT - from cryptojwt.key_jar import build_keyjar - -+from idpyoidc.key_import import import_jwks -+from idpyoidc.server.exception import UnAuthorizedClient -+ - from idpyoidc.client.defaults import DEFAULT_KEY_DEFS - from idpyoidc.defaults import JWT_BEARER - from idpyoidc.message.oidc import AccessTokenRequest -@@ -69,7 +72,7 @@ class TestEndpoint: - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], - } -- self.server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") -+ self.server.keyjar = import_jwks(self.server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") - self.session_manager = context.session_manager - self.token_endpoint = self.server.get_endpoint("token") - self.user_id = "diana" -@@ -206,7 +209,7 @@ class TestEndpoint: - _resp = self.token_endpoint.process_request(request=_req) - - # 2nd time used -- with pytest.raises(InvalidToken): -+ with pytest.raises((InvalidToken, UnAuthorizedClient)): - self.token_endpoint.parse_request(_token_request) - - def test_do_refresh_access_token(self): -diff --git a/tests/test_server_36_oauth2_token_exchange.py b/tests/test_server_36_oauth2_token_exchange.py -index 5b3a566..3a2b477 100644 ---- a/tests/test_server_36_oauth2_token_exchange.py -+++ b/tests/test_server_36_oauth2_token_exchange.py -@@ -5,6 +5,7 @@ import pytest - from cryptojwt.jwt import utc_time_sans_frac - from cryptojwt.key_jar import build_keyjar - -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oauth2 import TokenExchangeRequest - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -80,6 +81,7 @@ USERINFO = UserInfo(json.loads(open(full_path("users.json")).read())) - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -@@ -199,7 +201,7 @@ class TestEndpoint(object): - "response_types": ["code", "token", "code id_token", "id_token"], - "allowed_scopes": ["openid", "profile", "offline_access"], - } -- server.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") -+ server.keyjar = import_jwks(server.keyjar, CLIENT_KEYJAR.export_jwks(), "client_1") - self.endpoint = server.get_endpoint("token") - self.introspection_endpoint = server.get_endpoint("introspection") - self.session_manager = self.context.session_manager -@@ -646,8 +648,8 @@ class TestEndpoint(object): - _resp = self.endpoint.process_request(request=_req) - assert _resp["error"] == "invalid_request" - assert ( -- _resp["error_description"] -- == "Unsupported grant_type: urn:ietf:params:oauth:grant-type:token-exchange" -+ _resp["error_description"] -+ == "Unsupported grant_type: urn:ietf:params:oauth:grant-type:token-exchange" - ) - - def test_wrong_resource(self): -@@ -1374,7 +1376,7 @@ class TestEndpoint(object): - _resp = self.endpoint.process_request(request=_req) - assert _resp["error"] == "invalid_request" - assert ( -- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" -+ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" - ) - - token_exchange_req["scope"] = "offline_access" -@@ -1454,7 +1456,7 @@ class TestEndpoint(object): - _resp = self.endpoint.process_request(request=_req) - assert _resp["error"] == "invalid_request" - assert ( -- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" -+ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" - ) - - token_exchange_req["scope"] = "profile" -@@ -1466,7 +1468,7 @@ class TestEndpoint(object): - _resp = self.endpoint.process_request(request=_req) - assert _resp["error"] == "invalid_request" - assert ( -- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" -+ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" - ) - - token_exchange_req["scope"] = "offline_access" -@@ -1488,5 +1490,5 @@ class TestEndpoint(object): - _resp = self.endpoint.process_request(request=_req) - assert _resp["error"] == "invalid_request" - assert ( -- _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" -+ _resp["error_description"] == "Exchanging this subject token to refresh token forbidden" - ) -diff --git a/tests/test_server_38_oauth2_revocation_endpoint.py b/tests/test_server_38_oauth2_revocation_endpoint.py -index ad83af1..e41c539 100644 ---- a/tests/test_server_38_oauth2_revocation_endpoint.py -+++ b/tests/test_server_38_oauth2_revocation_endpoint.py -@@ -5,6 +5,7 @@ import pytest - from cryptojwt import as_unicode - from cryptojwt.utils import as_bytes - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import TokenRevocationRequest - from idpyoidc.message.oauth2 import TokenRevocationResponse - from idpyoidc.message.oidc import AccessTokenRequest -@@ -86,6 +87,7 @@ def full_path(local_file): - - @pytest.mark.parametrize("jwt_token", [True, False]) - class TestEndpoint: -+ - @pytest.fixture(autouse=True) - def create_endpoint(self, jwt_token): - conf = { -@@ -216,10 +218,8 @@ class TestEndpoint: - "research_and_scholarship", - ], - } -- endpoint_context.keyjar.import_jwks_as_json( -- endpoint_context.keyjar.export_jwks_as_json(private=True), -- endpoint_context.issuer, -- ) -+ endpoint_context.keyjar = store_under_other_id(endpoint_context.keyjar, "", -+ endpoint_context.issuer, True) - self.revocation_endpoint = server.get_endpoint("token_revocation") - self.token_endpoint = server.get_endpoint("token") - self.session_manager = endpoint_context.session_manager -diff --git a/tests/test_server_40_oauth2_pushed_authorization.py b/tests/test_server_40_oauth2_pushed_authorization.py -index 4d7ea6d..ef7d890 100644 ---- a/tests/test_server_40_oauth2_pushed_authorization.py -+++ b/tests/test_server_40_oauth2_pushed_authorization.py -@@ -7,6 +7,8 @@ from cryptojwt import JWT - from cryptojwt.jwt import remove_jwt_parameters - from cryptojwt.key_jar import init_key_jar - -+from idpyoidc.key_import import import_jwks -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message import Message - from idpyoidc.message.oauth2 import AuthorizationRequest - from idpyoidc.server import Server -@@ -73,6 +75,7 @@ AUTHN_REQUEST = ( - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - conf = { -@@ -167,11 +170,11 @@ class TestEndpoint(object): - context = server.context - _clients = yaml.safe_load(io.StringIO(client_yaml)) - context.cdb = verify_oidc_client_information(_clients["oidc_clients"]) -- server.keyjar.import_jwks(server.keyjar.export_jwks(True, ""), conf["issuer"]) -+ server.keyjar = store_under_other_id(server.keyjar, "", conf["issuer"], True) - - self.rp_keyjar = init_key_jar(key_defs=KEYDEFS, issuer_id="s6BhdRkqt3") - # Add RP's keys to the OP's keyjar -- server.keyjar.import_jwks(self.rp_keyjar.export_jwks(issuer_id="s6BhdRkqt3"), "s6BhdRkqt3") -+ server.keyjar = import_jwks(server.keyjar, self.rp_keyjar.export_jwks(issuer_id="s6BhdRkqt3"), "s6BhdRkqt3") - - self.pushed_authorization_endpoint = server.get_endpoint("pushed_authorization") - self.authorization_endpoint = server.get_endpoint("authorization") -@@ -251,7 +254,7 @@ class TestEndpoint(object): - - # And now for the authorization request with the OP provided request_uri - -- _msg["request_uri"] = _resp["http_response"]["request_uri"] -+ _msg["request_uri"] = _resp["response_args"]["request_uri"] - for parameter in ["code_challenge", "code_challenge_method"]: - del _msg[parameter] - -diff --git a/tests/test_server_50_persistence.py b/tests/test_server_50_persistence.py -index adc31d9..c8ca5a5 100644 ---- a/tests/test_server_50_persistence.py -+++ b/tests/test_server_50_persistence.py -@@ -6,6 +6,7 @@ import pytest - from cryptojwt.jwt import utc_time_sans_frac - from cryptojwt.key_jar import init_key_jar - -+from idpyoidc.key_import import import_jwks_as_json - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest - from idpyoidc.server import Server -@@ -212,6 +213,7 @@ ENDPOINT_CONTEXT_CONFIG = { - - - class TestEndpoint(object): -+ - @pytest.fixture(autouse=True) - def create_endpoint(self): - try: -@@ -221,9 +223,9 @@ class TestEndpoint(object): - - # Both have to use the same keyjar - _keyjar = init_key_jar(key_defs=KEYDEFS) -- _keyjar.import_jwks_as_json( -- _keyjar.export_jwks_as_json(True, ""), ENDPOINT_CONTEXT_CONFIG["issuer"] -- ) -+ _keyjar = import_jwks_as_json(_keyjar, -+ _keyjar.export_jwks_as_json(True, ""), -+ ENDPOINT_CONTEXT_CONFIG["issuer"]) - server1 = Server( - OPConfiguration(conf=ENDPOINT_CONTEXT_CONFIG, base_path=BASEDIR), - cwd=BASEDIR, -@@ -351,14 +353,15 @@ class TestEndpoint(object): - - def test_init(self): - assert self.endpoint[1] -- assert set(self.endpoint[1].upstream_get("context").provider_info["scopes_supported"]) == { -- "openid" -- } -+ -+ _context_1 = self.endpoint[1].upstream_get("context") -+ _context_2 = self.endpoint[2].upstream_get("context") -+ - assert ( -- self.endpoint[1].upstream_get("context").provider_info["claims_parameter_supported"] -- == self.endpoint[2].upstream_get("context").provider_info[ -- "claims_parameter_supported"] -+ _context_1.provider_info["claims_parameter_supported"] == _context_2.provider_info[ -+ "claims_parameter_supported"] - ) -+ print(_context_1.provider_info.get("claims_parameter_supported")) - - def test_parse(self): - session_id = self._create_session(AUTH_REQ, index=1) -diff --git a/tests/test_server_60_dpop.py b/tests/test_server_60_dpop.py -index e13b8a3..15ded45 100644 ---- a/tests/test_server_60_dpop.py -+++ b/tests/test_server_60_dpop.py -@@ -1,11 +1,14 @@ - import os - --import pytest - from cryptojwt.jwk.ec import ECKey - from cryptojwt.jwk.ec import new_ec_key - from cryptojwt.jws.jws import factory - from cryptojwt.key_jar import init_key_jar -+import pytest - -+from idpyoidc.client.defaults import DEFAULT_KEY_DEFS -+from idpyoidc.client.oauth2 import Client -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import AccessTokenRequest - from idpyoidc.message.oauth2 import AuthorizationRequest - from idpyoidc.server import Server -@@ -14,14 +17,16 @@ from idpyoidc.server.authn_event import create_authn_event - from idpyoidc.server.client_authn import verify_client - from idpyoidc.server.configure import OPConfiguration - from idpyoidc.server.oauth2.add_on.dpop import DPoPProof --from idpyoidc.server.oauth2.add_on.dpop import token_post_parse_request --from idpyoidc.server.oauth2.authorization import Authorization -+from idpyoidc.server.oidc.authorization import Authorization - from idpyoidc.server.oidc.token import Token -+from idpyoidc.server.oidc.userinfo import UserInfo - from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD - from idpyoidc.time_util import utc_time_sans_frac - from tests import CRYPT_CONFIG - from tests import SESSION_PARAMS - -+_dirname = os.path.dirname(os.path.abspath(__file__)) -+ - DPOP_HEADER = ( - "eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMz" - "R0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFq" -@@ -54,43 +59,13 @@ def test_verify_header(): - assert _dpop["htm"] == _dpop3["htm"] - - --KEYDEFS = [ -- {"type": "RSA", "key": "", "use": ["sig"]}, -- {"type": "EC", "crv": "P-256", "use": ["sig"]}, --] -- - ISSUER = "https://example.com/" - --KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) --KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") -- --RESPONSE_TYPES_SUPPORTED = [ -- ["code"], -- ["id_token"], -- ["code", "id_token"], --] -- --CAPABILITIES = { -- "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], -- "token_endpoint_auth_methods_supported": [ -- "client_secret_post", -- "client_secret_basic", -- "client_secret_jwt", -- "private_key_jwt", -- ], -- "response_modes_supported": ["query", "fragment", "form_post"], -- "subject_types_supported": ["public", "pairwise", "ephemeral"], -- "claim_types_supported": ["normal", "aggregated", "distributed"], -- "claims_parameter_supported": True, -- "request_parameter_supported": True, -- # "request_uri_parameter_supported": True, --} -+KEYJAR = init_key_jar(key_defs=DEFAULT_KEY_DEFS, issuer_id=ISSUER) -+KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) - - AUTH_REQ = AuthorizationRequest( -- client_id="client_1", -- redirect_uri="https://example.com/cb", - scope=["openid"], -- state="STATE", - response_type="code", - ) - -@@ -105,90 +80,174 @@ TOKEN_REQ = AccessTokenRequest( - BASEDIR = os.path.abspath(os.path.dirname(__file__)) - - --class TestEndpoint(object): -- @pytest.fixture(autouse=True) -- def create_endpoint(self): -- conf = { -- "issuer": ISSUER, -- "httpc_params": {"verify": False, "timeout": 1}, -- "capabilities": CAPABILITIES, -- "add_on": { -- "dpop": { -- "function": "idpyoidc.server.oauth2.add_on.dpop.add_support", -- "kwargs": {"dpop_signing_alg_values_supported": ["ES256"]}, -- }, -+def create_client(): -+ config = { -+ "client_id": "client_1", -+ "client_secret": "a longesh password", -+ "redirect_uris": ["https://example.com/cli/authz_cb"], -+ "preference": {"response_types": ["code"]}, -+ "add_ons": { -+ "dpop": { -+ "function": "idpyoidc.client.oauth2.add_on.dpop.add_support", -+ "kwargs": {"dpop_signing_alg_values_supported": ["ES256", "ES512"]}, -+ } -+ }, -+ "client_authn_methods": { -+ "dpop": { -+ "class": "idpyoidc.client.oauth2.add_on.dpop.DPoPClientAuth", -+ "kwargs": {} -+ } -+ } -+ } -+ -+ services = { -+ "discovery": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"}, -+ "authorization": {"class": "idpyoidc.client.oauth2.authorization.Authorization"}, -+ "access_token": {"class": "idpyoidc.client.oauth2.access_token.AccessToken"}, -+ "refresh_access_token": { -+ "class": "idpyoidc.client.oauth2.refresh_access_token.RefreshAccessToken" -+ }, -+ "userinfo": {"class": "idpyoidc.client.oidc.userinfo.UserInfo"}, -+ } -+ -+ CLI_KEY = init_key_jar( -+ public_path="{}/pub_client.jwks".format(_dirname), -+ private_path="{}/priv_client.jwks".format(_dirname), -+ key_defs=DEFAULT_KEY_DEFS, -+ issuer_id="client_id", -+ ) -+ -+ client = Client(keyjar=CLI_KEY, config=config, services=services) -+ -+ client.get_context().provider_info = { -+ "authorization_endpoint": "https://example.com/auth", -+ "token_endpoint": "https://example.com/token", -+ "dpop_signing_alg_values_supported": ["RS256", "ES256"], -+ "userinfo_endpoint": "https://example.com/user", -+ } -+ -+ return client -+ -+ -+def create_server(): -+ RESPONSE_TYPES_SUPPORTED = [ -+ ["code"], -+ ["id_token"], -+ ["code", "id_token"], -+ ] -+ -+ CAPABILITIES = { -+ "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], -+ "token_endpoint_auth_methods_supported": [ -+ "client_secret_post", -+ "client_secret_basic", -+ "client_secret_jwt", -+ "private_key_jwt", -+ ], -+ "response_modes_supported": ["query", "fragment", "form_post"], -+ "subject_types_supported": ["public", "pairwise", "ephemeral"], -+ "claim_types_supported": ["normal", "aggregated", "distributed"], -+ "claims_parameter_supported": True, -+ "request_parameter_supported": True, -+ # "request_uri_parameter_supported": True, -+ "client_authn_methods": { -+ "dpop": { -+ "class": "idpyoidc.server.oauth2.add_on.dpop.DPoPClientAuth" -+ } -+ } -+ } -+ -+ conf = { -+ "issuer": ISSUER, -+ "httpc_params": {"verify": False, "timeout": 1}, -+ "preference": CAPABILITIES, -+ "add_on": { -+ "dpop": { -+ "function": "idpyoidc.server.oauth2.add_on.dpop.add_support", -+ "kwargs": {"dpop_signing_alg_values_supported": ["ES256"]}, - }, -- "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, -- "token_handler_args": { -- "jwks_file": "private/token_jwks.json", -- "code": {"lifetime": 600, "kwargs": {"crypt_conf": CRYPT_CONFIG}}, -- "token": { -- "class": "idpyoidc.server.token.jwt_token.JWTToken", -- "kwargs": { -- "lifetime": 3600, -- "base_claims": {"eduperson_scoped_affiliation": None}, -- "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}, -- } -- }, -+ }, -+ "keys": {"uri_path": "jwks.json", "key_defs": DEFAULT_KEY_DEFS}, -+ "token_handler_args": { -+ "jwks_file": "private/token_jwks.json", -+ "code": {"lifetime": 600, "kwargs": {"crypt_conf": CRYPT_CONFIG}}, -+ "token": { -+ "class": "idpyoidc.server.token.jwt_token.JWTToken", -+ "kwargs": { -+ "lifetime": 3600, -+ "base_claims": {"eduperson_scoped_affiliation": None}, -+ "add_claims_by_scope": True, -+ "aud": ["https://example.org/appl"], - }, - }, -- "endpoint": { -- "authorization": { -- "path": "{}/authorization", -- "class": Authorization, -- "kwargs": {}, -+ "refresh": { -+ "class": "idpyoidc.server.token.jwt_token.JWTToken", -+ "kwargs": { -+ "lifetime": 3600, -+ "aud": ["https://example.org/appl"], - }, -- "token": { -- "path": "{}/token", -- "class": Token, -- "kwargs": {"client_authn_method": ["none"]}, -+ }, -+ "id_token": { -+ "class": "idpyoidc.server.token.id_token.IDToken", -+ "kwargs": { -+ "base_claims": { -+ "email": {"essential": True}, -+ "email_verified": {"essential": True}, -+ } - }, - }, -- "client_authn": verify_client, -- "authentication": { -- "anon": { -- "acr": INTERNETPROTOCOLPASSWORD, -- "class": "idpyoidc.server.user_authn.user.NoAuthn", -- "kwargs": {"user": "diana"}, -- } -+ }, -+ "endpoint": { -+ "authorization": { -+ "path": "{}/authorization", -+ "class": Authorization, -+ "kwargs": {}, - }, -- "template_dir": "template", -- "userinfo": { -- "class": user_info.UserInfo, -- "kwargs": {"db_file": "users.json"}, -+ "token": { -+ "path": "{}/token", -+ "class": Token, -+ "kwargs": {"client_authn_method": ["client_secret_basic"]}, - }, -- "session_params": SESSION_PARAMS, -- } -- server = Server(OPConfiguration(conf, base_path=BASEDIR), keyjar=KEYJAR) -- self.context = server.context -- self.context.cdb["client_1"] = { -- "client_secret": "hemligt", -- "redirect_uris": [("https://example.com/cb", None)], -- "client_salt": "salted", -- "token_endpoint_auth_method": "client_secret_post", -- "response_types": ["code", "token", "code id_token", "id_token"], -- "allowed_scopes": ["openid", "profile", "email", "address", "phone", "offline_access"], -- } -+ "user_info": { -+ "path": "{}/user", -+ "class": UserInfo, -+ "kwargs": {"client_authn_method": ["dpop"]}, -+ }, -+ }, -+ "client_authn": verify_client, -+ "authentication": { -+ "anon": { -+ "acr": INTERNETPROTOCOLPASSWORD, -+ "class": "idpyoidc.server.user_authn.user.NoAuthn", -+ "kwargs": {"user": "diana"}, -+ } -+ }, -+ "template_dir": "template", -+ "userinfo": { -+ "class": user_info.UserInfo, -+ "kwargs": {"db_file": "users.json"}, -+ }, -+ "session_params": SESSION_PARAMS, -+ } -+ server = Server(OPConfiguration(conf, base_path=BASEDIR), keyjar=KEYJAR) -+ return server -+ -+ -+class TestEndpoint(object): -+ @pytest.fixture(autouse=True) -+ def create_setup(self): -+ self.server = create_server() - self.user_id = "diana" -- self.token_endpoint = server.get_endpoint("token") -+ self.token_endpoint = self.server.get_endpoint("token") -+ self.user_info_endpoint = self.server.get_endpoint("userinfo") -+ -+ self.client = create_client() -+ self.context = self.server.context -+ self.context.cdb["client_1"] = self.client.context.prefers() - self.session_manager = self.context.session_manager - -+ self.authz_service = self.client.get_service("authorization") -+ - def _create_session(self, auth_req, sub_type="public", sector_identifier=""): - if sector_identifier: - authz_req = auth_req.copy() -@@ -201,72 +260,79 @@ class TestEndpoint(object): - ae, authz_req, 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") -- -+ def _mint_token(self, token_class, grant, session_id, based_on=None, **kwargs): - # Constructing an authorization code is now done -- _code = grant.mint_token( -+ return grant.mint_token( - session_id=session_id, -- context=self.context, -- token_class="authorization_code", -- token_handler=self.session_manager.token_handler["authorization_code"], -- usage_rules=usage_rules, -+ context=self.token_endpoint.upstream_get("context"), -+ token_class=token_class, -+ token_handler=self.session_manager.token_handler.handler[token_class], -+ expires_at=utc_time_sans_frac() + 300, # 5 minutes from now -+ based_on=based_on, -+ **kwargs - ) - -- 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 _access_token_request_response(self): -+ # Authz -+ auth_req = AUTH_REQ.copy() -+ auth_req["client_id"] = self.client.client_id -+ _redirect_uri = self.client.context.claims.get_preference("redirect_uris")[0] -+ auth_req["redirect_uri"] = _redirect_uri -+ _context = self.client.context -+ auth_req["state"] = _context.cstate.create_state(iss=_context.get("issuer")) -+ session_id = self._create_session(auth_req) -+ # Consent handling -+ grant = self.token_endpoint.upstream_get("endpoint_context").authz(session_id, auth_req) -+ self.session_manager[session_id] = grant -+ code = self._mint_token("authorization_code", grant, session_id) -+ _context.cstate.update(auth_req["state"], auth_req) -+ -+ # Access token request from the RP -+ token_serv = self.client.get_service("accesstoken") -+ req_args = { -+ "grant_type": "authorization_code", -+ "code": code.value, -+ "redirect_uri": _redirect_uri -+ } -+ req_info = token_serv.get_request_parameters(request_args=req_args, state=auth_req["state"]) -+ assert "headers" in req_info -+ assert "dpop" in req_info["headers"] -+ -+ # On the OP's side -+ req = self.token_endpoint.parse_request( -+ req_args, -+ http_info={"headers": req_info["headers"], "url": _redirect_uri, "method": "POST"}) -+ resp = self.token_endpoint.process_request(req) -+ _context.cstate.update(auth_req["state"], resp["response_args"]) -+ return resp, auth_req["state"] - - def test_post_parse_request(self): -- auth_req = token_post_parse_request( -- AUTH_REQ, -- AUTH_REQ["client_id"], -- self.context, -- http_info={ -- "headers": {"dpop": DPOP_HEADER}, -- "url": "https://server.example.com/token", -- "method": "POST", -- }, -- ) -- assert auth_req -- assert "dpop_jkt" in auth_req -+ # DPoP Access Token Request -+ _response, state = self._access_token_request_response() -+ assert "response_args" in _response - - def test_process_request(self): -- session_id = self._create_session(AUTH_REQ) -- grant = self.session_manager[session_id] -- code = self._mint_code(grant, AUTH_REQ["client_id"]) -- -- _token_request = TOKEN_REQ.to_dict() -- _context = self.context -- _token_request["code"] = code.value -- _req = self.token_endpoint.parse_request( -- _token_request, -- http_info={ -- "headers": {"dpop": DPOP_HEADER}, -- "url": "https://server.example.com/token", -- "method": "POST", -- }, -- ) -+ _response, state = self._access_token_request_response() -+ -+ # The RP creates the user info request -+ _user_info_service = self.client.get_service("userinfo") -+ _request = _user_info_service.get_request_parameters(state=state, authn_method="dpop") - -- assert "dpop_jkt" in _req -+ http_info = { -+ "headers": _request["headers"], -+ "method": _request["method"], -+ "url": _request["url"] -+ } - -- _resp = self.token_endpoint.process_request(request=_req) -- assert _resp["response_args"]["token_type"] == "DPoP" -+ assert set(http_info["headers"].keys()) == {"Authorization", "dpop"} -+ assert http_info["headers"]["Authorization"].startswith("DPoP ") - -- access_token = _resp["response_args"]["access_token"] -- jws = factory(access_token) -- _payload = jws.jwt.payload() -- assert "cnf" in _payload -- assert _payload["cnf"]["jkt"] == _req["dpop_jkt"] -+ _jws = factory(http_info["headers"]["dpop"]) -+ _payload = _jws.jwt.payload() -+ assert "htm" in _payload -+ assert "htu" in _payload - -- # Make sure DPoP also is in the session access token instance. -- _session_info = self.session_manager.get_session_info_by_token( -- access_token, handler_key="access_token" -- ) -- _token = self.session_manager.find_token(_session_info["branch_id"], access_token) -- assert _token.token_type == "DPoP" -+ _req = self.user_info_endpoint.parse_request(request=_request, http_info=http_info) -+ _resp = self.user_info_endpoint.process_request(_req) -+ assert _resp["response_args"] -+ assert "sub" in _resp["response_args"] -diff --git a/tests/test_server_61_add_on.py b/tests/test_server_61_add_on.py -index 9af7206..ac91951 100644 ---- a/tests/test_server_61_add_on.py -+++ b/tests/test_server_61_add_on.py -@@ -4,6 +4,7 @@ from urllib.parse import urlparse - import pytest - from cryptojwt.key_jar import init_key_jar - -+from idpyoidc.key_import import store_under_other_id - from idpyoidc.message.oauth2 import AuthorizationRequest - from idpyoidc.message.oauth2 import AuthorizationResponse - from idpyoidc.server import Server -@@ -24,7 +25,7 @@ KEYDEFS = [ - ISSUER = "https://example.com/" - - KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) --KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") -+KEYJAR = store_under_other_id(KEYJAR, ISSUER, "", True) - - RESPONSE_TYPES_SUPPORTED = [ - ["code"], -diff --git a/tests/test_tandem_oauth2_add_on.py b/tests/test_tandem_oauth2_add_on.py -index a3776fc..000ab8a 100644 ---- a/tests/test_tandem_oauth2_add_on.py -+++ b/tests/test_tandem_oauth2_add_on.py -@@ -5,6 +5,7 @@ from typing import List - from cryptojwt.key_jar import build_keyjar - - from idpyoidc.client.oauth2 import Client -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oauth2 import is_error_message - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -169,6 +170,7 @@ CLIENT_CONFIG = { - - - class Flow(object): -+ - def __init__(self, client, server): - self.client = client - self.server = server -@@ -290,7 +292,7 @@ def test_pkce(): - ) - - server.context.cdb["client"] = CLIENT_CONFIG -- server.context.keyjar.import_jwks(client.keyjar.export_jwks(), "client") -+ server.context.keyjar = import_jwks(server.context.keyjar, client.keyjar.export_jwks(), "client") - - server.context.set_provider_info() - -@@ -332,7 +334,7 @@ def test_jar(): - ) - - server.context.cdb["client"] = CLIENT_CONFIG -- server.context.keyjar.import_jwks(client.keyjar.export_jwks(), "client") -+ server.context.keyjar = import_jwks(server.context.keyjar, client.keyjar.export_jwks(), "client") - - server.context.set_provider_info() - -diff --git a/tests/test_tandem_oauth2_code.py b/tests/test_tandem_oauth2_code.py -index 091a004..39bc376 100644 ---- a/tests/test_tandem_oauth2_code.py -+++ b/tests/test_tandem_oauth2_code.py -@@ -5,6 +5,7 @@ import pytest - from cryptojwt.key_jar import build_keyjar - - from idpyoidc.client.oauth2 import Client -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oauth2 import is_error_message - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -13,7 +14,6 @@ from idpyoidc.server import Server - from idpyoidc.server.authz import AuthzHandling - from idpyoidc.server.client_authn import verify_client - from idpyoidc.server.configure import ASConfiguration --from idpyoidc.server.cookie_handler import CookieHandler - from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD - from idpyoidc.server.user_info import UserInfo - from idpyoidc.util import rndstr -@@ -72,6 +72,7 @@ _OAUTH2_SERVICES = { - - - class TestFlow(object): -+ - @pytest.fixture(autouse=True) - def create_entities(self): - server_conf = { -@@ -170,7 +171,7 @@ class TestFlow(object): - - self.context = self.server.context - self.context.cdb["client_1"] = client_1_config -- self.context.keyjar.import_jwks(self.client.keyjar.export_jwks(), "client_1") -+ self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") - - self.context.set_provider_info() - self.session_manager = self.context.session_manager -diff --git a/tests/test_tandem_oauth2_par_service.py b/tests/test_tandem_oauth2_par_service.py -new file mode 100644 -index 0000000..9630a55 ---- /dev/null -+++ b/tests/test_tandem_oauth2_par_service.py -@@ -0,0 +1,285 @@ -+import json -+import os -+ -+import pytest -+from cryptojwt.key_jar import build_keyjar -+ -+from idpyoidc.client.oauth2 import Client -+from idpyoidc.key_import import import_jwks -+from idpyoidc.message.oauth2 import is_error_message -+from idpyoidc.message.oidc import AccessTokenRequest -+from idpyoidc.message.oidc import AuthorizationRequest -+from idpyoidc.message.oidc import RefreshAccessTokenRequest -+from idpyoidc.server import Server -+from idpyoidc.server.authz import AuthzHandling -+from idpyoidc.server.client_authn import verify_client -+from idpyoidc.server.configure import ASConfiguration -+from idpyoidc.server.user_authn.authn_context import INTERNETPROTOCOLPASSWORD -+from idpyoidc.server.user_info import UserInfo -+from idpyoidc.util import rndstr -+from tests import CRYPT_CONFIG -+from tests import SESSION_PARAMS -+ -+KEYDEFS = [ -+ {"type": "RSA", "key": "", "use": ["sig"]}, -+ {"type": "EC", "crv": "P-256", "use": ["sig"]}, -+] -+ -+CLIENT_KEYJAR = build_keyjar(KEYDEFS) -+ -+COOKIE_KEYDEFS = [ -+ {"type": "oct", "kid": "sig", "use": ["sig"]}, -+ {"type": "oct", "kid": "enc", "use": ["enc"]}, -+] -+ -+AUTH_REQ = AuthorizationRequest( -+ client_id="client_1", -+ redirect_uri="https://example.com/cb", -+ scope=["openid"], -+ state="STATE", -+ response_type="code", -+) -+ -+TOKEN_REQ = AccessTokenRequest( -+ client_id="client_1", -+ redirect_uri="https://example.com/cb", -+ state="STATE", -+ grant_type="authorization_code", -+ client_secret="hemligt", -+) -+ -+REFRESH_TOKEN_REQ = RefreshAccessTokenRequest( -+ grant_type="refresh_token", client_id="https://example.com/", client_secret="hemligt" -+) -+ -+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 = UserInfo(json.loads(open(full_path("users.json")).read())) -+ -+_OAUTH2_SERVICES = { -+ "metadata": {"class": "idpyoidc.client.oauth2.server_metadata.ServerMetadata"}, -+ "authorization": {"class": "idpyoidc.client.oauth2.authorization.Authorization"}, -+ "pushed_authorization": {"class": "idpyoidc.client.oauth2.pushed_authorization.PushedAuthorization"}, -+ "access_token": {"class": "idpyoidc.client.oauth2.access_token.AccessToken"}, -+ "resource": {"class": "idpyoidc.client.oauth2.resource.Resource"}, -+} -+ -+ -+class TestFlow(object): -+ -+ @pytest.fixture(autouse=True) -+ def create_entities(self): -+ server_conf = { -+ "issuer": "https://example.com/", -+ "httpc_params": {"verify": False, "timeout": 1}, -+ "subject_types_supported": ["public", "pairwise", "ephemeral"], -+ "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, -+ "endpoint": { -+ "metadata": { -+ "path": ".well-known/oauth-authorization-server", -+ "class": "idpyoidc.server.oauth2.server_metadata.ServerMetadata", -+ "kwargs": {}, -+ }, -+ "authorization": { -+ "path": "authorization", -+ "class": "idpyoidc.server.oauth2.authorization.Authorization", -+ "kwargs": {}, -+ }, -+ "pushed_authorization": { -+ "path": "par", -+ "class": "idpyoidc.server.oauth2.pushed_authorization.PushedAuthorization", -+ "kwargs": {}, -+ }, -+ "token": { -+ "path": "token", -+ "class": "idpyoidc.server.oauth2.token.Token", -+ "kwargs": {}, -+ }, -+ }, -+ "authentication": { -+ "anon": { -+ "acr": INTERNETPROTOCOLPASSWORD, -+ "class": "idpyoidc.server.user_authn.user.NoAuthn", -+ "kwargs": {"user": "diana"}, -+ } -+ }, -+ "userinfo": {"class": UserInfo, "kwargs": {"db": {}}}, -+ "client_authn": verify_client, -+ "authz": { -+ "class": AuthzHandling, -+ "kwargs": { -+ "grant_config": { -+ "usage_rules": { -+ "authorization_code": { -+ "supports_minting": ["access_token", "refresh_token"], -+ "max_usage": 1, -+ }, -+ "access_token": { -+ "supports_minting": ["access_token", "refresh_token"], -+ "expires_in": 600, -+ }, -+ "refresh_token": { -+ "supports_minting": ["access_token"], -+ "audience": ["https://example.com", "https://example2.com"], -+ "expires_in": 43200, -+ }, -+ }, -+ "expires_in": 43200, -+ } -+ }, -+ }, -+ "token_handler_args": { -+ "jwks_file": "private/token_jwks.json", -+ "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"], -+ }, -+ }, -+ }, -+ "session_params": SESSION_PARAMS, -+ } -+ self.server = Server(ASConfiguration(conf=server_conf, base_path=BASEDIR), cwd=BASEDIR) -+ -+ client_1_config = { -+ "issuer": server_conf["issuer"], -+ "client_secret": "hemligtlösenord", -+ "client_id": "client_1", -+ "redirect_uris": ["https://example.com/cb"], -+ "client_salt": "salted_peanuts_cooking", -+ "token_endpoint_auth_methods_supported": ["client_secret_post"], -+ "response_types_supported": ["code"], -+ } -+ client_services = _OAUTH2_SERVICES -+ self.client = Client( -+ client_type="oauth2", -+ config=client_1_config, -+ keyjar=build_keyjar(KEYDEFS), -+ services=_OAUTH2_SERVICES, -+ ) -+ -+ self.context = self.server.context -+ self.context.cdb["client_1"] = client_1_config -+ self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") -+ -+ self.context.set_provider_info() -+ self.session_manager = self.context.session_manager -+ self.user_id = "diana" -+ -+ def do_query(self, service_type, endpoint_type, request_args, state): -+ _client_service = self.client.get_service(service_type) -+ req_info = _client_service.get_request_parameters(request_args=request_args, state=state) -+ -+ areq = req_info.get("request") -+ headers = req_info.get("headers") -+ -+ _server_endpoint = self.server.get_endpoint(endpoint_type) -+ if areq: -+ if headers: -+ argv = {"http_info": {"headers": headers}} -+ else: -+ argv = {} -+ areq.lax = True -+ _req = areq.serialize(_server_endpoint.request_format) -+ _pr_resp = _server_endpoint.parse_request(_req, **argv) -+ else: -+ _pr_resp = _server_endpoint.parse_request(areq) -+ -+ if is_error_message(_pr_resp): -+ return areq, _pr_resp -+ -+ _resp = _server_endpoint.process_request(_pr_resp) -+ if is_error_message(_resp): -+ return areq, _resp -+ -+ _response = _server_endpoint.do_response(**_resp) -+ -+ resp = _client_service.parse_response(_response["response"]) -+ _client_service.update_service_context(_resp["response_args"], key=state) -+ return areq, resp -+ -+ def process_setup(self, token=None, scope=None): -+ # ***** Discovery ********* -+ -+ _req, _resp = self.do_query("server_metadata", "server_metadata", {}, "") -+ -+ # ***** Pushed Authorization Request ********** -+ _nonce = (rndstr(24),) -+ _context = self.client.get_service_context() -+ # Need a new state for a new authorization request -+ _state = _context.cstate.create_state(iss=_context.get("issuer")) -+ _context.cstate.bind_key(_nonce, _state) -+ -+ req_args = {"response_type": ["code"], "nonce": _nonce, "state": _state} -+ -+ if scope: -+ _scope = scope -+ else: -+ _scope = ["openid"] -+ -+ if token and list(token.keys())[0] == "refresh_token": -+ _scope = ["openid", "offline_access"] -+ -+ req_args["scope"] = _scope -+ -+ areq, auth_response = self.do_query("pushed_authorization", -+ "pushed_authorization", -+ req_args, -+ _state) -+ -+ # ***** Authorization Request ********** -+ _context = self.client.get_service_context() -+ -+ req_args = {"request_uri": auth_response["request_uri"], "response_type": ["code"]} -+ -+ areq, auth_response = self.do_query("authorization", "authorization", req_args, _state) -+ -+ # ***** Token Request ********** -+ -+ req_args = { -+ "code": auth_response["code"], -+ "state": auth_response["state"], -+ "redirect_uri": areq["redirect_uri"], -+ "grant_type": "authorization_code", -+ "client_id": self.client.get_client_id(), -+ "client_secret": _context.get_usage("client_secret"), -+ } -+ -+ _token_request, resp = self.do_query("accesstoken", "token", req_args, _state) -+ -+ return resp, _state, _scope -+ -+ def test_flow(self): -+ """ -+ Test that token exchange requests work correctly -+ """ -+ -+ resp, _state, _scope = self.process_setup(token="access_token", scope=["foobar"]) -+ -+ # Construct the resource request -+ -+ _client_service = self.client.get_service("resource") -+ req_info = _client_service.get_request_parameters( -+ authn_method="bearer_header", state=_state, endpoint="https://resource.example.com" -+ ) -+ -+ assert req_info["url"] == "https://resource.example.com" -+ assert "Authorization" in req_info["headers"] -+ assert req_info["headers"]["Authorization"].startswith("Bearer") -diff --git a/tests/test_tandem_oauth2_token_exchange.py b/tests/test_tandem_oauth2_token_exchange.py -index 6b722d3..15e9e67 100644 ---- a/tests/test_tandem_oauth2_token_exchange.py -+++ b/tests/test_tandem_oauth2_token_exchange.py -@@ -5,6 +5,7 @@ import pytest - from cryptojwt.key_jar import build_keyjar - - from idpyoidc.client.oauth2 import Client -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oauth2 import is_error_message - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -212,8 +213,8 @@ class TestEndpoint(object): - self.context = self.server.context - self.context.cdb["client_1"] = client_1_config - self.context.cdb["client_2"] = client_2_config -- self.context.keyjar.import_jwks(self.client_1.keyjar.export_jwks(), "client_1") -- self.context.keyjar.import_jwks(self.client_2.keyjar.export_jwks(), "client_2") -+ self.context.keyjar = import_jwks(self.context.keyjar, self.client_1.keyjar.export_jwks(), "client_1") -+ self.context.keyjar = import_jwks(self.context.keyjar, self.client_2.keyjar.export_jwks(), "client_2") - - self.context.set_provider_info() - -diff --git a/tests/test_tandem_oauth2_token_revocation.py b/tests/test_tandem_oauth2_token_revocation.py -index 92d4430..6845cd9 100644 ---- a/tests/test_tandem_oauth2_token_revocation.py -+++ b/tests/test_tandem_oauth2_token_revocation.py -@@ -4,6 +4,7 @@ import pytest - from cryptojwt.key_jar import build_keyjar - - from idpyoidc.client.oauth2 import Client -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oauth2 import is_error_message - from idpyoidc.server import ASConfiguration - from idpyoidc.server import Server -@@ -137,7 +138,7 @@ class TestClient(object): - # ------- tell the server about the client ---------------- - self.context = self.server.context - self.context.cdb["client_1"] = client_conf -- self.context.keyjar.import_jwks(self.client.keyjar.export_jwks(), "client_1") -+ self.context.keyjar = import_jwks(self.context.keyjar, self.client.keyjar.export_jwks(), "client_1") - - def do_query(self, service_type, endpoint_type, request_args, state): - _client = self.client.get_service(service_type) -diff --git a/tests/test_tandem_oidc_code.py b/tests/test_tandem_oidc_code.py -index 5f11f4a..9b575aa 100644 ---- a/tests/test_tandem_oidc_code.py -+++ b/tests/test_tandem_oidc_code.py -@@ -5,6 +5,7 @@ import pytest - from cryptojwt.key_jar import build_keyjar - - from idpyoidc.client.oidc import RP -+from idpyoidc.key_import import import_jwks - from idpyoidc.message.oauth2 import is_error_message - from idpyoidc.message.oidc import AccessTokenRequest - from idpyoidc.message.oidc import AuthorizationRequest -@@ -74,6 +75,7 @@ _OIDC_SERVICES = { - - - class TestFlow(object): -+ - @pytest.fixture(autouse=True) - def create_entities(self): - server_conf = { -@@ -81,6 +83,7 @@ class TestFlow(object): - "httpc_params": {"verify": False, "timeout": 1}, - "subject_types_supported": ["public", "pairwise", "ephemeral"], - "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, -+ "scopes_supported": ["openid", "profile", "email", "offline_access", "address", "phone"], - "endpoint": { - "provider_info": { - "path": ".well-known/openid-configuration", -@@ -180,12 +183,13 @@ class TestFlow(object): - "redirect_uris": ["https://example.com/cb"], - "token_endpoint_auth_methods_supported": ["client_secret_post"], - "response_types_supported": ["code", "id_token", "id_token token"], -+ "preference": {"scopes_supported": ["openid", "profile"]} - } - self.rp = RP(config=client_config, keyjar=build_keyjar(KEYDEFS), services=_OIDC_SERVICES) - - self.context = self.server.context - # self.context.cdb["client_1"] = client_config -- # self.context.keyjar.import_jwks(self.rp.keyjar.export_jwks(), "client_1") -+ # self.context.keyjar = import_jwks(self.context.keyjar, self.rp.keyjar.export_jwks(), "client_1") - - self.context.set_provider_info() - # self.session_manager = self.context.session_manager -@@ -225,10 +229,11 @@ class TestFlow(object): - _client_service.update_service_context(_resp["response_args"], key=state) - # Fake key import - if service_type == "provider_info": -- _client_service.upstream_get("attribute", "keyjar").import_jwks( -- _server_endpoint.upstream_get("attribute", "keyjar").export_jwks(), -- issuer_id=_server_endpoint.upstream_get("attribute", "issuer"), -- ) -+ _keyjar = _client_service.upstream_get("attribute", "keyjar") -+ _keyjar = import_jwks(_keyjar, -+ _server_endpoint.upstream_get("attribute", "keyjar").export_jwks(), -+ _server_endpoint.upstream_get("attribute", "issuer")) -+ - return areq, resp - - def process_setup(self, token=None, scope=None): -@@ -252,10 +257,13 @@ class TestFlow(object): - if scope: - _scope = scope - else: -- _scope = ["openid"] -- -- if token and list(token.keys())[0] == "refresh_token": -- _scope = ["openid", "offline_access"] -+ _scope = _context.claims.get_usage("scope", None) -+ if not _scope: -+ if token: -+ if isinstance(token, list) and list(token.keys())[0] == "refresh_token": -+ _scope = ["openid", "offline_access"] -+ else: -+ _scope = ["openid"] - - req_args["scope"] = _scope - -@@ -281,13 +289,11 @@ class TestFlow(object): - Test that token exchange requests work correctly - """ - -- resp, _state, _scope = self.process_setup( -- token="access_token", -- scope=["openid", "profile", "email", "address", "phone", "offline_access"], -- ) -+ resp, _state, _scope = self.process_setup(token="access_token") - - # The User Info request - - _request, resp = self.do_query("userinfo", "userinfo", {}, _state) - - assert resp -+ assert "given_name" in resp -diff --git a/tests/test_y_actor_01.py b/tests/test_y_actor_01.py -deleted file mode 100644 -index e69de29..0000000 -diff --git a/tests/x_test_ciba_01_backchannel_auth.py b/tests/x_test_ciba_01_backchannel_auth.py -index 62d79ac..bc4f67f 100644 ---- a/tests/x_test_ciba_01_backchannel_auth.py -+++ b/tests/x_test_ciba_01_backchannel_auth.py -@@ -487,133 +487,133 @@ CLI_KEY = init_key_jar( - ) - - --class TestBCAEndpointService(object): -- @pytest.fixture(autouse=True) -- def create_endpoint(self): -- self.ciba = {"self.server": self._create_self.server(), "client": self._create_ciba_client()} -- -- def _create_self.server(self): -- self.server = Server(OPConfiguration(SERVER_CONF, base_path=BASEDIR)) -- context = self.server.context -- context.cdb["client_1"] = { -- "client_secret": "hemligt", -- "redirect_uris": [("https://example.com/cb", None)], -- "client_salt": "salted", -- "token_endpoint_auth_method": "client_secret_post", -- "response_types": ["code", "token", "code id_token", "id_token"], -- } -- -- client_keyjar = build_keyjar(KEYDEFS) -- # Add self.servers keys -- client_keyjar.import_jwks(self.server.keyjar.export_jwks(), ISSUER) -- # The only own key the client has a this point -- client_keyjar.add_symmetric("", CLIENT_SECRET, ["sig"]) -- # Need to add the client_secret as a symmetric key bound to the client_id -- self.server.keyjar.add_symmetric(CLIENT_ID, CLIENT_SECRET, ["sig"]) -- self.server.keyjar.import_jwks(client_keyjar.export_jwks(), CLIENT_ID) -- -- self.server.context.cdb = {CLIENT_ID: {"client_secret": CLIENT_SECRET}} -- # login_hint -- self.server.context.login_hint_lookup = init_service( -- {"class": "idpyoidc.self.server.login_hint.LoginHintLookup"}, None -- ) -- # userinfo -- _userinfo = init_user_info( -- { -- "class": "idpyoidc.self.server.user_info.UserInfo", -- "kwargs": {"db_file": full_path("users.json")}, -- }, -- "", -- ) -- self.server.context.login_hint_lookup.userinfo = _userinfo -- return self.server -- -- def _create_ciba_client(self): -- config = { -- "client_id": CLIENT_ID, -- "client_secret": CLIENT_SECRET, -- "redirect_uris": ["https://example.com/cb"], -- "services": { -- "client_notification": { -- "class": "idpyoidc.client.oidc.backchannel_authentication.ClientNotification", -- "kwargs": {"conf": {"default_authn_method": "client_notification_authn"}}, -- }, -- }, -- "client_authn_methods": { -- "client_notification_authn": { -- 'class': "idpyoidc.client.oidc.backchannel_authentication.ClientNotificationAuthn" -- } -- }, -- } -- -- client = Client(keyjar=CLI_KEY, config=config, services=DEFAULT_OAUTH2_SERVICES) -- -- client.upstream_get("context").provider_info = { -- "client_notification_endpoint": "https://example.com/notify", -- } -- -- return client -- -- def _create_session(self, user_id, auth_req, sub_type="public", sector_identifier=""): -- if sector_identifier: -- authz_req = auth_req.copy() -- authz_req["sector_identifier_uri"] = sector_identifier -- else: -- authz_req = auth_req -- client_id = authz_req["client_id"] -- ae = create_authn_event(user_id) -- _session_manager = self.ciba["self.server"].context.session_manager -- return _session_manager.create_session( -- ae, authz_req, user_id, client_id=client_id, sub_type=sub_type -- ) -- -- def test_client_notification(self): -- _keyjar = self.ciba["self.server"].context.keyjar -- _jwt = JWT(_keyjar, iss=CLIENT_ID, sign_alg="HS256") -- _jwt.with_jti = True -- _assertion = _jwt.pack({"aud": [ISSUER]}) -- -- request = { -- "client_assertion": _assertion, -- "client_assertion_type": JWT_BEARER, -- "scope": "openid email example-scope", -- "client_notification_token": "8d67dc78-7faa-4d41-aabd-67707b374255", -- "binding_message": "W4SCT", -- "login_hint": "mail:diana@example.org", -- } -- -- _authn_endpoint = self.ciba["self.server"].upstream_get("endpoint", "backchannel_authentication") -- -- req = AuthenticationRequest(**request) -- req = _authn_endpoint.parse_request(req.to_urlencoded()) -- _info = _authn_endpoint.process_request(req) -- assert _info -- -- _session_manager = self.ciba["self.server"].context.session_manager -- sid = _session_manager.auth_req_id_map[_info["response_args"]["auth_req_id"]] -- _user_id, _client_id, _grant_id = _session_manager.decrypt_session_id(sid) -- -- # Some time passes and the client authentication is successfully performed -- # The interaction with the authentication device is not shown -- session_id_2 = self._create_session(_user_id, req) -- -- # Now it's time to send a client notification -- req_args = { -- "auth_req_id": _info["response_args"]["auth_req_id"], -- "client_notification_token": request["client_notification_token"], -- } -- -- _service = self.ciba["client"].upstream_get("service", "client_notification") -- _req_param = _service.get_request_parameters(request_args=req_args) -- assert _req_param -- assert isinstance(_req_param["request"], NotificationRequest) -- assert set(_req_param.keys()) == {"method", "request", "url", "body", "headers"} -- assert _req_param["method"] == "POST" -- # This is the client's notification endpoint -- assert ( -- _req_param["url"] -- == self.ciba["client"] -- .upstream_get("context") -- .provider_info["client_notification_endpoint"] -- ) -- assert set(_req_param["request"].keys()) == {"auth_req_id", "client_notification_token"} -+# class TestBCAEndpointServi ce(object): -+# @pytest.fixture(autouse=True) -+# def create_endpoint(self): -+# self.ciba = {"self.server": self._create_self.server(), "client": self._create_ciba_client()} -+# -+# def _create_self.server(self): -+# self.server = Server(OPConfiguration(SERVER_CONF, base_path=BASEDIR)) -+# context = self.server.context -+# context.cdb["client_1"] = { -+# "client_secret": "hemligt", -+# "redirect_uris": [("https://example.com/cb", None)], -+# "client_salt": "salted", -+# "token_endpoint_auth_method": "client_secret_post", -+# "response_types": ["code", "token", "code id_token", "id_token"], -+# } -+# -+# client_keyjar = build_keyjar(KEYDEFS) -+# # Add self.servers keys -+# client_keyjar.import_jwks(self.server.keyjar.export_jwks(), ISSUER) -+# # The only own key the client has a this point -+# client_keyjar.add_symmetric("", CLIENT_SECRET, ["sig"]) -+# # Need to add the client_secret as a symmetric key bound to the client_id -+# self.server.keyjar.add_symmetric(CLIENT_ID, CLIENT_SECRET, ["sig"]) -+# self.server.keyjar.import_jwks(client_keyjar.export_jwks(), CLIENT_ID) -+# -+# self.server.context.cdb = {CLIENT_ID: {"client_secret": CLIENT_SECRET}} -+# # login_hint -+# self.server.context.login_hint_lookup = init_service( -+# {"class": "idpyoidc.self.server.login_hint.LoginHintLookup"}, None -+# ) -+# # userinfo -+# _userinfo = init_user_info( -+# { -+# "class": "idpyoidc.self.server.user_info.UserInfo", -+# "kwargs": {"db_file": full_path("users.json")}, -+# }, -+# "", -+# ) -+# self.server.context.login_hint_lookup.userinfo = _userinfo -+# return self.server -+# -+# def _create_ciba_client(self): -+# config = { -+# "client_id": CLIENT_ID, -+# "client_secret": CLIENT_SECRET, -+# "redirect_uris": ["https://example.com/cb"], -+# "services": { -+# "client_notification": { -+# "class": "idpyoidc.client.oidc.backchannel_authentication.ClientNotification", -+# "kwargs": {"conf": {"default_authn_method": "client_notification_authn"}}, -+# }, -+# }, -+# "client_authn_methods": { -+# "client_notification_authn": { -+# 'class': "idpyoidc.client.oidc.backchannel_authentication.ClientNotificationAuthn" -+# } -+# }, -+# } -+# -+# client = Client(keyjar=CLI_KEY, config=config, services=DEFAULT_OAUTH2_SERVICES) -+# -+# client.upstream_get("context").provider_info = { -+# "client_notification_endpoint": "https://example.com/notify", -+# } -+# -+# return client -+# -+# def _create_session(self, user_id, auth_req, sub_type="public", sector_identifier=""): -+# if sector_identifier: -+# authz_req = auth_req.copy() -+# authz_req["sector_identifier_uri"] = sector_identifier -+# else: -+# authz_req = auth_req -+# client_id = authz_req["client_id"] -+# ae = create_authn_event(user_id) -+# _session_manager = self.ciba["self.server"].context.session_manager -+# return _session_manager.create_session( -+# ae, authz_req, user_id, client_id=client_id, sub_type=sub_type -+# ) -+# -+# def test_client_notification(self): -+# _keyjar = self.ciba["self.server"].context.keyjar -+# _jwt = JWT(_keyjar, iss=CLIENT_ID, sign_alg="HS256") -+# _jwt.with_jti = True -+# _assertion = _jwt.pack({"aud": [ISSUER]}) -+# -+# request = { -+# "client_assertion": _assertion, -+# "client_assertion_type": JWT_BEARER, -+# "scope": "openid email example-scope", -+# "client_notification_token": "8d67dc78-7faa-4d41-aabd-67707b374255", -+# "binding_message": "W4SCT", -+# "login_hint": "mail:diana@example.org", -+# } -+# -+# _authn_endpoint = self.ciba["self.server"].upstream_get("endpoint", "backchannel_authentication") -+# -+# req = AuthenticationRequest(**request) -+# req = _authn_endpoint.parse_request(req.to_urlencoded()) -+# _info = _authn_endpoint.process_request(req) -+# assert _info -+# -+# _session_manager = self.ciba["self.server"].context.session_manager -+# sid = _session_manager.auth_req_id_map[_info["response_args"]["auth_req_id"]] -+# _user_id, _client_id, _grant_id = _session_manager.decrypt_session_id(sid) -+# -+# # Some time passes and the client authentication is successfully performed -+# # The interaction with the authentication device is not shown -+# session_id_2 = self._create_session(_user_id, req) -+# -+# # Now it's time to send a client notification -+# req_args = { -+# "auth_req_id": _info["response_args"]["auth_req_id"], -+# "client_notification_token": request["client_notification_token"], -+# } -+# -+# _service = self.ciba["client"].upstream_get("service", "client_notification") -+# _req_param = _service.get_request_parameters(request_args=req_args) -+# assert _req_param -+# assert isinstance(_req_param["request"], NotificationRequest) -+# assert set(_req_param.keys()) == {"method", "request", "url", "body", "headers"} -+# assert _req_param["method"] == "POST" -+# # This is the client's notification endpoint -+# assert ( -+# _req_param["url"] -+# == self.ciba["client"] -+# .upstream_get("context") -+# .provider_info["client_notification_endpoint"] -+# ) -+# assert set(_req_param["request"].keys()) == {"auth_req_id", "client_notification_token"} diff --git a/patch/transform.patch b/patch/transform.patch deleted file mode 100644 index b79ae0f5..00000000 --- a/patch/transform.patch +++ /dev/null @@ -1,60 +0,0 @@ -diff --git a/src/idpyoidc/client/claims/transform.py b/src/idpyoidc/transform.py -similarity index 94% -rename from src/idpyoidc/client/claims/transform.py -rename to src/idpyoidc/transform.py -index 1ca40c6..3834006 100644 ---- a/src/idpyoidc/client/claims/transform.py -+++ b/src/idpyoidc/transform.py -@@ -51,10 +51,10 @@ REQUEST2REGISTER = { - - - def supported_to_preferred( -- supported: dict, -- preference: dict, -- base_url: str, -- info: Optional[dict] = None, -+ supported: dict, -+ preference: dict, -+ base_url: str, -+ info: Optional[dict] = None, - ): - if info: # The provider info - for key, val in supported.items(): -@@ -83,7 +83,7 @@ def supported_to_preferred( - preference[key] = [x for x in val if x in _info_val] - else: - pass -- else: -+ elif val: - preference[key] = val - - # special case -> must have a request_uris value -@@ -148,7 +148,7 @@ def _intersection(a, b): - - - def preferred_to_registered( -- prefers: dict, supported: dict, registration_response: Optional[dict] = None -+ prefers: dict, supported: dict, registration_response: Optional[dict] = None - ): - """ - The claims with values that are returned from the OP is what goes unless (!!) -@@ -200,7 +200,7 @@ def preferred_to_registered( - # be a singleton or an array. So just add it as is. - registered[_reg_key] = val - -- logger.debug(f"Entity registered: {registered}") -+ logger.debug(f"preferred2registered: {registered}") - return registered - - -@@ -219,4 +219,10 @@ def create_registration_request(prefers: dict, supported: dict) -> dict: - continue - - _request[key] = array_or_singleton(spec, value) -+ -+ for key, val in prefers.items(): -+ if key not in RegistrationRequest.c_param.keys(): -+ if key not in REGISTER2PREFERRED.values(): -+ _request[key] = val -+ - return _request From 1c5546cd563828f51634ec0e8405ea5595b2b6de Mon Sep 17 00:00:00 2001 From: roland Date: Wed, 4 Dec 2024 09:55:37 +0100 Subject: [PATCH 20/21] Accept both response_types_supported and response_types for registering. Metadata before and after registration may differ. When doing client authentication using basic auth the client ID may be an url. Allow registration class to be passed as an argument. --- src/idpyoidc/claims.py | 27 +++++++++++++++++++++ src/idpyoidc/client/oidc/registration.py | 3 +-- src/idpyoidc/client/service_context.py | 2 ++ src/idpyoidc/server/client_authn.py | 2 +- src/idpyoidc/server/oauth2/authorization.py | 6 +++-- src/idpyoidc/transform.py | 8 +++--- 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/idpyoidc/claims.py b/src/idpyoidc/claims.py index afa6680f..45f54bea 100644 --- a/src/idpyoidc/claims.py +++ b/src/idpyoidc/claims.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) + def claims_dump(info, exclude_attributes): return {qualified_name(info.__class__): info.dump(exclude_attributes=exclude_attributes)} @@ -321,3 +322,29 @@ def get_client_metadata(self, return {entity_type: metadata} else: return metadata + + def get_registration_metadata(self, + entity_type: Optional[str] = "", + metadata_schema: Optional[Message] = None, + extra_claims: Optional[List[str]] = None, + supported: Optional[dict] = None, + **kwargs): + + metadata = self.prefer + + # the claims that can appear in the metadata + if metadata_schema: + attr = list(metadata_schema.c_param.keys()) + else: + attr = [] + + if extra_claims: + attr.extend(extra_claims) + + if attr: + metadata = {k: v for k, v in metadata.items() if k in attr} + + if entity_type: + return {entity_type: metadata} + else: + return metadata diff --git a/src/idpyoidc/client/oidc/registration.py b/src/idpyoidc/client/oidc/registration.py index 435c7f7c..e0a1363d 100644 --- a/src/idpyoidc/client/oidc/registration.py +++ b/src/idpyoidc/client/oidc/registration.py @@ -103,8 +103,7 @@ def gather_request_args(self, **kwargs): @return: """ _context = self.upstream_get("context") - req_args = _context.claims.get_client_metadata(metadata_schema=self.msg_type, - supported=_context.supports()) + req_args = _context.claims.create_registration_request() if "request_args" in self.conf: req_args.update(self.conf["request_args"]) diff --git a/src/idpyoidc/client/service_context.py b/src/idpyoidc/client/service_context.py index b4d391c2..3959a43e 100644 --- a/src/idpyoidc/client/service_context.py +++ b/src/idpyoidc/client/service_context.py @@ -386,6 +386,8 @@ def prefer_or_support(self, claim): return None def map_supported_to_preferred(self, info: Optional[dict] = None): + # goes from what the entity can do to something the opponent could handle + # info is metadata for the opponent if known self.claims.prefer = supported_to_preferred( self.supports(), self.claims.prefer, base_url=self.base_url, info=info ) diff --git a/src/idpyoidc/server/client_authn.py b/src/idpyoidc/server/client_authn.py index 8a0c72da..c713b155 100755 --- a/src/idpyoidc/server/client_authn.py +++ b/src/idpyoidc/server/client_authn.py @@ -99,7 +99,7 @@ def basic_authn(authorization_token: str): _tok = as_bytes(authorization_token[6:]) # Will raise ValueError type exception if not base64 encoded _tok = base64.b64decode(_tok) - part = as_unicode(_tok).split(":", 1) + part = as_unicode(_tok).rsplit(":", 1) if len(part) != 2: raise ValueError("Illegal token") diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index e2cd4fa7..6c2fc5aa 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -430,8 +430,10 @@ def verify_response_type(self, request: Union[Message, dict], cinfo: dict) -> bo # Checking response types _registered = [set(rt.split(" ")) for rt in cinfo.get("response_types_supported", [])] if not _registered: - # If no response_type is registered by the client then we'll use code. - _registered = [{"code"}] + _registered = [set(rt.split(" ")) for rt in cinfo.get("response_types", [])] + if not _registered: + # If no response_type is registered by the client then we'll use code. + _registered = [{"code"}] # Is the asked for response_type among those that are permitted return set(request["response_type"]) in _registered diff --git a/src/idpyoidc/transform.py b/src/idpyoidc/transform.py index 3834006c..7a9b5593 100644 --- a/src/idpyoidc/transform.py +++ b/src/idpyoidc/transform.py @@ -1,6 +1,7 @@ import logging from typing import Optional +from idpyoidc.message import Message from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.message.oidc import RegistrationResponse @@ -204,9 +205,10 @@ def preferred_to_registered( return registered -def create_registration_request(prefers: dict, supported: dict) -> dict: +def create_registration_request(prefers: dict, supported: dict, + registration_class: Optional[Message] = RegistrationRequest) -> dict: _request = {} - for key, spec in RegistrationRequest.c_param.items(): + for key, spec in registration_class.c_param.items(): _pref_key = REGISTER2PREFERRED.get(key, key) if _pref_key in prefers: value = prefers[_pref_key] @@ -221,7 +223,7 @@ def create_registration_request(prefers: dict, supported: dict) -> dict: _request[key] = array_or_singleton(spec, value) for key, val in prefers.items(): - if key not in RegistrationRequest.c_param.keys(): + if key not in registration_class.c_param.keys(): if key not in REGISTER2PREFERRED.values(): _request[key] = val From e39a95b07372679e8322137e674f8bf1b637fac3 Mon Sep 17 00:00:00 2001 From: roland Date: Thu, 5 Dec 2024 09:53:11 +0100 Subject: [PATCH 21/21] Client authentication at the authorization endpoint is based on having been authenticated at the PushedAuthorization endpoint. --- src/idpyoidc/server/client_authn.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/idpyoidc/server/client_authn.py b/src/idpyoidc/server/client_authn.py index c713b155..69a0a512 100755 --- a/src/idpyoidc/server/client_authn.py +++ b/src/idpyoidc/server/client_authn.py @@ -427,6 +427,30 @@ def _verify( return {"client_id": client_id, "jwt": _jwt} +class PushedAuthorization(ClientAuthnMethod): + # The premise here is that there has been a client authentication at the + # pushed authorization endpoint + tag = "pushed_authz" + + def is_usable(self, request=None, authorization_token=None, http_info: Optional[dict] = None): + _request_uri = request.get("request_uri", None) + if _request_uri: + _context = self.upstream_get("context") + if _request_uri.startswith("urn:uuid:") and _request_uri in _context.par_db: + return True + + def _verify( + self, + request: Optional[Union[dict, Message]] = None, + authorization_token: Optional[str] = None, + endpoint=None, # Optional[Endpoint] + http_info: Optional[dict] = None, + **kwargs, + ): + client_id = request["client_id"] + return {"client_id": client_id} + + CLIENT_AUTHN_METHOD = dict( client_secret_basic=ClientSecretBasic, client_secret_post=ClientSecretPost, @@ -437,6 +461,7 @@ def _verify( request_param=RequestParam, public=PublicAuthn, none=NoneAuthn, + pushed_authz=PushedAuthorization ) TYPE_METHOD = [(JWT_BEARER, JWSAuthnMethod)]