text: import copy
  import hashlib
  import logging
  from typing import Dict
  from urllib.parse import urlsplit
  
  from oic import oic
  from oic import rndstr
  from oic.exception import MissingAttribute
  from oic.oauth2 import ErrorResponse
  from oic.oauth2 import ResponseError
  from oic.oauth2 import TokenError
  from oic.oauth2.message import ASConfigurationResponse
  from oic.oic import AuthorizationRequest
  from oic.oic import AuthorizationResponse
  from oic.oic import RegistrationResponse
  from oic.oic.message import OpenIDSchema
  from oic.utils.authn.client import CLIENT_AUTHN_METHOD
  from oic.utils.http_util import Redirect
  from oic.utils.sanitize import sanitize
  
  __author__ = "roland"
  
  
  logger = logging.getLogger(__name__)
  
  
  class OIDCError(Exception):
      pass
  
  
  class Client(oic.Client):
      def __init__(
          self,
          client_id=None,
          client_prefs=None,
          client_authn_method=None,
          keyjar=None,
          verify_ssl=True,
          behaviour=None,
          config=None,
          jwks_uri="",
          kid=None,
      ):
          oic.Client.__init__(
              self,
              client_id,
              client_prefs,
              client_authn_method,
              keyjar,
              verify_ssl,
              config=config,
          )
          if behaviour:
              self.behaviour = behaviour
          self.userinfo_request_method = ""
          self.allow_sign_alg_none = False
          self.authz_req = {}
          self.get_userinfo = True
          self.oidc = True
          self.jwks_uri = jwks_uri
          self.kid = kid
  
      def create_authn_request(self, session, acr_value=None, **kwargs):
          session["state"] = rndstr(32)
          request_args = {
              "response_type": self.behaviour["response_type"],
              "scope": self.behaviour["scope"],
              "state": session["state"],
              "redirect_uri": self.registration_response["redirect_uris"][0],
          }
  
          if self.oidc:
              session["nonce"] = rndstr(32)
              request_args["nonce"] = session["nonce"]
  
          if acr_value is not None:
              request_args["acr_values"] = acr_value
  
          request_args.update(kwargs)
          cis = self.construct_AuthorizationRequest(request_args=request_args)
          logger.debug("request: %s" % sanitize(cis))
  
          url, body, ht_args, cis = self.uri_and_body(
              AuthorizationRequest, cis, method="GET", request_args=request_args
          )
  
          self.authz_req[request_args["state"]] = cis
          logger.debug("body: %s" % sanitize(body))
          logger.info("URL: %s" % sanitize(url))
          logger.debug("ht_args: %s" % sanitize(ht_args))
  
          resp = Redirect(str(url))
          if ht_args:
              resp.headers.extend([(a, b) for a, b in ht_args.items()])
          logger.debug("resp_headers: %s" % sanitize(resp.headers))
          return resp
  
      def has_access_token(self, **kwargs):
          try:
              token = self.get_token(**kwargs)
          except TokenError:
              pass
          else:
              if token.access_token:
                  return True
          return False
  
      def _err(self, txt):
          logger.error(sanitize(txt))
          raise OIDCError(txt)
  
      def _do_code(self, response, authresp):
          """Perform code flow."""
          # get the access token
          try:
              args = {
                  "code": authresp["code"],
                  "redirect_uri": self.registration_response["redirect_uris"][0],
                  "client_id": self.client_id,
                  "client_secret": self.client_secret,
              }
  
              try:
                  args["scope"] = response["scope"]
              except KeyError:
                  pass
  
              atresp = self.do_access_token_request(
                  state=authresp["state"],
                  request_args=args,
                  authn_method=self.registration_response["token_endpoint_auth_method"],
              )
              msg = "Access token response: {}"
              logger.info(msg.format(sanitize(atresp)))
          except Exception as err:
              logger.error("%s" % err)
              raise
  
          if isinstance(atresp, ErrorResponse):
              msg = "Error response: {}"
              self._err(msg.format(sanitize(atresp.to_dict())))
  
          _token = atresp["access_token"]
  
          _id_token = atresp.get("id_token")
          return _token, _id_token
  
      def callback(self, response, session, format="dict"):
          """
          Call when an AuthN response has been received from the OP.
  
          :param response: The URL returned by the OP
          :return:
          """
          try:
              authresp = self.parse_response(
                  AuthorizationResponse, response, sformat=format, keyjar=self.keyjar
              )
          except ResponseError:
              msg = "Could not parse response: '{}'"
              logger.error(msg.format(sanitize(response)))
              raise OIDCError("Problem parsing response")
  
          logger.info("AuthorizationReponse: {}".format(sanitize(authresp)))
          if isinstance(authresp, ErrorResponse):
              if authresp["error"] == "login_required":
                  return self.create_authn_request(session)
              else:
                  raise OIDCError("Access denied")
  
          _state = authresp["state"]
  
          _id_token = authresp.get("id_token")
          if (
              _id_token is not None
              and _id_token["nonce"] != self.authz_req[_state]["nonce"]
          ):
              self._err("Received nonce not the same as expected.")
  
          if self.behaviour["response_type"] == "code":
              _token, new_id_token = self._do_code(response, authresp)
              if new_id_token is not None:
                  _id_token = new_id_token
          else:
              _token = authresp["access_token"]
  
          if not self.oidc:
              return {"access_token": _token}
  
          if _id_token is None:
              self._err("Invalid response: no IdToken")
  
          if _id_token["iss"] != self.provider_info["issuer"]:
              self._err("Issuer mismatch")
  
          if _id_token["nonce"] != self.authz_req[_state]["nonce"]:
              self._err("Nonce mismatch")
  
          if not self.allow_sign_alg_none:
              if _id_token.jws_header["alg"] == "none":
                  self._err('Do not allow "none" signature algorithm')
  
          user_id = "{}:{}".format(_id_token["iss"], _id_token["sub"])
  
          if self.get_userinfo:
              if self.userinfo_request_method:
                  kwargs = {"method": self.userinfo_request_method}
              else:
                  kwargs = {}
  
              if self.has_access_token(state=authresp["state"]):
                  inforesp = self.do_user_info_request(state=authresp["state"], **kwargs)
  
                  if isinstance(inforesp, ErrorResponse):
                      self._err("Invalid response %s." % inforesp["error"])
  
                  userinfo = inforesp.to_dict()
  
                  if _id_token["sub"] != userinfo["sub"]:
                      self._err("Invalid response: userid mismatch")
  
                  logger.debug("UserInfo: %s" % sanitize(inforesp))
  
                  try:
                      self.id_token[user_id] = _id_token
                  except TypeError:
                      self.id_token = {user_id: _id_token}
              else:
                  userinfo = {}
                  for attr in OpenIDSchema.c_param:
                      try:
                          userinfo[attr] = _id_token[attr]
                      except KeyError:
                          pass
  
              return {
                  "user_id": user_id,
                  "userinfo": userinfo,
                  "id_token": _id_token,
                  "access_token": _token,
              }
          else:
              return {"user_id": user_id, "id_token": _id_token, "access_token": _token}
  
  
  class OIDCClients(object):
      def __init__(self, config, base_url, seed="", jwks_info=None, verify_ssl=True):
          """
          Initialize the client.
  
          :param config: Imported configuration module
          :return:
          """
          self.client: Dict[str, Client] = {}
          self.client_cls = Client
          self.config = config
          self.seed = seed or rndstr(16)
          self.seed = self.seed.encode("utf8")
          self.path: Dict[str, str] = {}
          self.base_url = base_url
          self.jwks_info = jwks_info
          self.verify_ssl = verify_ssl
  
          for key, val in config.CLIENTS.items():
              if self.jwks_info:
                  _val = val.copy()
                  _val.update(self.jwks_info)
              else:
                  _val = val
  
              if key == "":
                  continue
              else:
                  self.client[key] = self.create_client(**_val)
  
      def get_path(self, redirect_uris, issuer):
          for ruri in redirect_uris:
              p = urlsplit(ruri)
              self.path[p.path[1:]] = issuer
  
      def create_client(self, userid="", **kwargs):
          """
          Do an instantiation of a client instance.
  
          :param userid: An identifier of the user
          :param: Keyword arguments
              Keys are ["srv_discovery_url", "client_info", "client_registration",
              "provider_info"]
          :return: client instance
          """
          _key_set = set(list(kwargs.keys()))
          try:
              _verify_ssl = kwargs["verify_ssl"]
          except KeyError:
              _verify_ssl = self.verify_ssl
          else:
              _key_set.discard("verify_ssl")
  
          client = self.client_cls(
              client_authn_method=CLIENT_AUTHN_METHOD,
              behaviour=kwargs["behaviour"],
              verify_ssl=_verify_ssl,
          )
  
          try:
              client.userinfo_request_method = kwargs["userinfo_request_method"]
          except KeyError:
              pass
          else:
              _key_set.discard("userinfo_request_method")
  
          # The behaviour parameter is not significant for the election process
          _key_set.discard("behaviour")
          for param in ["allow"]:
              try:
                  setattr(client, param, kwargs[param])
              except KeyError:
                  pass
              else:
                  _key_set.discard(param)
  
          if _key_set == {"client_info"}:  # Everything dynamic
              # There has to be a userid
              if not userid:
                  raise MissingAttribute("Missing userid specification")
  
              # Find the service that provides information about the OP
              issuer = client.wf.discovery_query(userid)
              # Gather OP information
              client.provider_config(issuer)
              # register the client
              client.register(
                  client.provider_info["registration_endpoint"], **kwargs["client_info"]
              )
              self.get_path(kwargs["client_info"]["redirect_uris"], issuer)
          elif _key_set == set(["client_info", "srv_discovery_url"]):
              # Ship the webfinger part
              # Gather OP information
              client.provider_config(kwargs["srv_discovery_url"])
              # register the client
              client.register(
                  client.provider_info["registration_endpoint"], **kwargs["client_info"]
              )
              self.get_path(
                  kwargs["client_info"]["redirect_uris"], kwargs["srv_discovery_url"]
              )
          elif _key_set == set(["provider_info", "client_info"]):
              client.handle_provider_config(
                  ASConfigurationResponse(**kwargs["provider_info"]),
                  kwargs["provider_info"]["issuer"],
              )
              client.register(
                  client.provider_info["registration_endpoint"], **kwargs["client_info"]
              )
  
              self.get_path(
                  kwargs["client_info"]["redirect_uris"],
                  kwargs["provider_info"]["issuer"],
              )
          elif _key_set == set(["provider_info", "client_registration"]):
              client.handle_provider_config(
                  ASConfigurationResponse(**kwargs["provider_info"]),
                  kwargs["provider_info"]["issuer"],
              )
              client.store_registration_info(
                  RegistrationResponse(**kwargs["client_registration"])
              )
              self.get_path(
                  kwargs["client_info"]["redirect_uris"],
                  kwargs["provider_info"]["issuer"],
              )
          elif _key_set == set(["srv_discovery_url", "client_registration"]):
              client.provider_config(kwargs["srv_discovery_url"])
              client.store_registration_info(
                  RegistrationResponse(**kwargs["client_registration"])
              )
              self.get_path(
                  kwargs["client_registration"]["redirect_uris"],
                  kwargs["srv_discovery_url"],
              )
          else:
              raise Exception("Configuration error ?")
  
          return client
  
      def dynamic_client(self, userid="", issuer=""):
          client = self.client_cls(
              client_authn_method=CLIENT_AUTHN_METHOD,
              verify_ssl=self.verify_ssl,
              **self.jwks_info,
          )
  
          if userid:
              issuer = client.wf.discovery_query(userid)
  
          if not issuer:
              raise OIDCError("Missing issuer")
  
          logger.info("issuer: {}".format(issuer))
          if issuer in self.client:
              return self.client[issuer]
          else:
              # Gather OP information
              _pcr = client.provider_config(issuer)
              logger.info("Provider info: {}".format(sanitize(_pcr.to_dict())))
              # register the client
              _cinfo = self.config.CLIENTS[""]["client_info"]
              reg_args = copy.copy(_cinfo)
              h = hashlib.sha256(self.seed)
              h.update(issuer.encode("utf8"))  # issuer has to be bytes
              base_urls = _cinfo["redirect_uris"]
  
              reg_args["redirect_uris"] = [
                  u.format(base=self.base_url, iss=h.hexdigest()) for u in base_urls
              ]
              try:
                  reg_args["post_logout_redirect_uris"] = [
                      u.format(base=self.base_url, iss=h.hexdigest())
                      for u in reg_args["post_logout_redirect_uris"]
                  ]
              except KeyError:
                  pass
  
              self.get_path(reg_args["redirect_uris"], issuer)
              if client.jwks_uri:
                  reg_args["jwks_uri"] = client.jwks_uri
  
              rr = client.register(_pcr["registration_endpoint"], **reg_args)
              msg = "Registration response: {}"
              logger.info(msg.format(sanitize(rr.to_dict())))
  
              try:
                  client.behaviour.update(**self.config.CLIENTS[""]["behaviour"])
              except KeyError:
                  pass
  
              self.client[issuer] = client
              return client
  
      def __getitem__(self, item):
          """
          Given a service or user identifier return a suitable client.
  
          :param item:
          :return:
          """
          try:
              return self.client[item]
          except KeyError:
              return self.dynamic_client(issuer=item)
  
      def keys(self):
          return list(self.client.keys())
  
      def return_paths(self):
          return self.path.keys()
  