From 0e0e70ba149f87aa319ca3e8894b599f58dde349 Mon Sep 17 00:00:00 2001 From: roland Date: Mon, 25 Nov 2024 19:55:48 +0100 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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",