text: import base64
  import logging
  from typing import Dict, List, Optional, Union
  
  from django.conf import settings
  from django.contrib.auth import get_user_model, logout
  from django.contrib.auth.mixins import LoginRequiredMixin
  from django.core.exceptions import (ImproperlyConfigured, ObjectDoesNotExist,
                                      PermissionDenied, ValidationError)
  from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
  from django.template.backends.django import Template
  from django.template.exceptions import (TemplateDoesNotExist,
                                          TemplateSyntaxError)
  from django.template.loader import get_template
  from django.urls import reverse
  from django.utils.datastructures import MultiValueDictKeyError
  from django.utils.decorators import method_decorator
  from django.utils.module_loading import import_string
  from django.utils.translation import gettext as _
  from django.views import View
  from django.views.decorators.cache import never_cache
  from django.views.decorators.csrf import csrf_exempt
  from django.views.decorators.http import require_http_methods
  from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
  from saml2.authn_context import PASSWORD, AuthnBroker, authn_context_class_ref
  from saml2.ident import NameID
  from saml2.saml import NAMEID_FORMAT_UNSPECIFIED
  
  from .error_views import error_cbv
  from .idp import IDP
  from .models import ServiceProvider
  from .processors import BaseProcessor
  from .utils import repr_saml, verify_request_signature
  
  logger = logging.getLogger(__name__)
  
  User = get_user_model()
  
  
  def store_params_in_session(request: HttpRequest) -> None:
      """ Gathers the SAML parameters from the HTTP request and store them in the session
      """
      if request.method == 'POST':
          # future TODO: parse also SOAP and PAOS format from POST
          passed_data = request.POST
          binding = BINDING_HTTP_POST
      else:
          passed_data = request.GET
          binding = BINDING_HTTP_REDIRECT
  
      try:
          saml_request = passed_data['SAMLRequest']
      except (KeyError, MultiValueDictKeyError) as e:
          raise ValidationError(_('not a valid SAMLRequest: {}').format(repr(e)))
  
      request.session['Binding'] = binding
      request.session['SAMLRequest'] = saml_request
      request.session['RelayState'] = passed_data.get('RelayState', '')
  
  
  @never_cache
  @csrf_exempt
  @require_http_methods(["GET", "POST"])
  def sso_entry(request: HttpRequest, *args, **kwargs) -> HttpResponse:
      """ Entrypoint view for SSO. Store the saml info in the request session
          and redirects to the login_process view.
      """
      try:
          store_params_in_session(request)
      except ValidationError as e:
          return error_cbv.handle_error(request, e, status_code=400)
  
      logger.debug("SSO requested to IDP with binding {}".format(request.session['Binding']))
      logger.debug("--- SAML request [\n{}] ---".format(repr_saml(request.session['SAMLRequest'], b64=True)))
  
      return HttpResponseRedirect(reverse('djangosaml2idp:saml_login_process'))
  
  
  def check_access(processor: BaseProcessor, request: HttpRequest) -> None:
      """ Check if user has access to the service of this SP. Raises a PermissionDenied exception if not.
      """
      if not processor.has_access(request):
          raise PermissionDenied(_("You do not have access to this resource"))
  
  
  def get_sp_config(sp_entity_id: str) -> ServiceProvider:
      """ Get a dict with the configuration for a SP according to the SAML_IDP_SPCONFIG settings.
          Raises an exception if no SP matching the given entity id can be found.
      """
      try:
          sp = ServiceProvider.objects.get(entity_id=sp_entity_id, active=True)
      except ObjectDoesNotExist:
          raise ImproperlyConfigured(_("No active Service Provider object matching the entity_id '{}' found").format(sp_entity_id))
      return sp
  
  
  def get_authn(req_info=None):
      req_authn_context = req_info.message.requested_authn_context if req_info else PASSWORD
      broker = AuthnBroker()
      broker.add(authn_context_class_ref(req_authn_context), "")
      return broker.get_authn_by_accr(req_authn_context)
  
  
  def build_authn_response(user: User, authn, resp_args, service_provider: ServiceProvider) -> list:  # type: ignore
      """ pysaml2 server.Server.create_authn_response wrapper
      """
      policy = resp_args.get('name_id_policy', None)
      if policy is None:
          name_id_format = NAMEID_FORMAT_UNSPECIFIED
      else:
          name_id_format = policy.format
  
      idp_server = IDP.load()
      idp_name_id_format_list = idp_server.config.getattr("name_id_format", "idp") or [NAMEID_FORMAT_UNSPECIFIED]
  
      if name_id_format not in idp_name_id_format_list:
          raise ImproperlyConfigured(_('SP requested a name_id_format that is not supported in the IDP: {}').format(name_id_format))
  
      processor: BaseProcessor = service_provider.processor  # type: ignore
      user_id = processor.get_user_id(user, name_id_format, service_provider, idp_server.config)
      name_id = NameID(format=name_id_format, sp_name_qualifier=service_provider.entity_id, text=user_id)
  
      return idp_server.create_authn_response(
          authn=authn,
          identity=processor.create_identity(user, service_provider.attribute_mapping),
          name_id=name_id,
          userid=user_id,
          sp_entity_id=service_provider.entity_id,
          # Signing
          sign_response=service_provider.sign_response,
          sign_assertion=service_provider.sign_assertion,
          sign_alg=service_provider.signing_algorithm,
          digest_alg=service_provider.digest_algorithm,
          # Encryption
          encrypt_assertion=service_provider.encrypt_saml_responses,
          encrypted_advice_attributes=service_provider.encrypt_saml_responses,
          **resp_args
      )
  
  
  class IdPHandlerViewMixin:
      """ Contains some methods used by multiple views """
  
      def render_login_html_to_string(self, context=None, request=None, using=None):
          """ Render the html response for the login action. Can be using a custom html template if set on the view. """
          default_login_template_name = 'djangosaml2idp/login.html'
          custom_login_template_name = getattr(self, 'login_html_template', None)
  
          if custom_login_template_name:
              template = self._fetch_custom_template(custom_login_template_name, default_login_template_name, using)
              return template.render(context, request)
  
          template = get_template(default_login_template_name, using=using)
          return template.render(context, request)
  
      @staticmethod
      def _fetch_custom_template(custom_name: str, default_name: str, using: Optional[str] = None) -> Template:
          """ Grabs the custom login template. Falls back to default if issues arise. """
          try:
              template = get_template(custom_name, using=using)
          except (TemplateDoesNotExist, TemplateSyntaxError) as e:
              logger.error(
                  'Specified template {} cannot be used due to: {}. Falling back to default login template {}'.format(
                      custom_name, str(e), default_name))
              template = get_template(default_name, using=using)
          return template
  
      def create_html_response(self, request: HttpRequest, binding, authn_resp, destination, relay_state):
          """ Login form for SSO
          """
          if binding == BINDING_HTTP_POST:
              context = {
                  "acs_url": destination,
                  "saml_response": base64.b64encode(str(authn_resp).encode()).decode(),
                  "relay_state": relay_state,
              }
              html_response = {
                  "data": self.render_login_html_to_string(context=context, request=request),
                  "type": "POST",
              }
          else:
              idp_server = IDP.load()
              http_args = idp_server.apply_binding(
                  binding=binding,
                  msg_str=authn_resp,
                  destination=destination,
                  relay_state=relay_state,
                  response=True)
  
              logger.debug('http args are: %s' % http_args)
              html_response = {
                  "data": http_args['headers'][0][1],
                  "type": "REDIRECT",
              }
          return html_response
  
      def render_response(self, request: HttpRequest, html_response, processor: BaseProcessor = None) -> HttpResponse:
          """ Return either a response as redirect to MultiFactorView or as html with self-submitting form to log in.
          """
          if not processor:
              # In case of SLO, where processor isn't relevant
              if html_response['type'] == 'POST':
                  return HttpResponse(html_response['data'])
              else:
                  return HttpResponseRedirect(html_response['data'])
  
          request.session['saml_data'] = html_response
  
          if processor.enable_multifactor(request.user):
              logger.debug("Redirecting to process_multi_factor")
              return HttpResponseRedirect(reverse('djangosaml2idp:saml_multi_factor'))
  
          # No multifactor
          logger.debug("Performing SAML redirect")
          if html_response['type'] == 'POST':
              return HttpResponse(html_response['data'])
          else:
              return HttpResponseRedirect(html_response['data'])
  
  
  @method_decorator(never_cache, name='dispatch')
  class LoginProcessView(LoginRequiredMixin, IdPHandlerViewMixin, View):
      """ View which processes the actual SAML request and returns a self-submitting form with the SAML response.
          The login_required decorator ensures the user authenticates first on the IdP using 'normal' ways.
      """
  
      def get(self, request, *args, **kwargs):
          binding = request.session.get('Binding', BINDING_HTTP_POST)
  
          # TODO: would it be better to store SAML info in request objects?
          # AuthBackend takes request obj as argument...
          try:
              idp_server = IDP.load()
  
              # Parse incoming request
              req_info = idp_server.parse_authn_request(request.session['SAMLRequest'], binding)
  
              # check SAML request signature
              try:
                  verify_request_signature(req_info)
              except ValueError as excp:
                  return error_cbv.handle_error(request, exception=excp, status_code=400)
  
              # Compile Response Arguments
              resp_args = idp_server.response_args(req_info.message)
              # Set SP and Processor
              sp_entity_id = resp_args.pop('sp_entity_id')
              service_provider = get_sp_config(sp_entity_id)
              # Check if user has access
              try:
                  # Check if user has access to SP
                  check_access(service_provider.processor, request)
              except PermissionDenied as excp:
                  return error_cbv.handle_error(request, exception=excp, status_code=403)
              # Construct SamlResponse message
              authn_resp = build_authn_response(request.user, get_authn(), resp_args, service_provider)
          except Exception as e:
              return error_cbv.handle_error(request, exception=e, status_code=500)
  
          html_response = self.create_html_response(
              request,
              binding=resp_args['binding'],
              authn_resp=authn_resp,
              destination=resp_args['destination'],
              relay_state=request.session['RelayState'])
  
          logger.debug("--- SAML Authn Response [\n{}] ---".format(repr_saml(str(authn_resp))))
          return self.render_response(request, html_response, service_provider.processor)
  
  
  @method_decorator(never_cache, name='dispatch')
  class SSOInitView(LoginRequiredMixin, IdPHandlerViewMixin, View):
      """ View used for IDP initialized login, doesn't handle any SAML authn request
      """
  
      def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
          return self.get(request, *args, **kwargs)
  
      def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
          request_data = request.POST or request.GET
          passed_data: Dict[str, Union[str, List[str]]] = request_data.copy().dict()
  
          try:
              # get sp information from the parameters
              sp_entity_id = str(passed_data['sp'])
              service_provider = get_sp_config(sp_entity_id)
              processor: BaseProcessor = service_provider.processor  # type: ignore
          except (KeyError, ImproperlyConfigured) as excp:
              return error_cbv.handle_error(request, exception=excp, status_code=400)
  
          try:
              # Check if user has access to SP
              check_access(processor, request)
          except PermissionDenied as excp:
              return error_cbv.handle_error(request, exception=excp, status_code=403)
  
          idp_server = IDP.load()
  
          binding_out, destination = idp_server.pick_binding(
              service="assertion_consumer_service",
              entity_id=sp_entity_id)
  
          # Adding a few things that would have been added if this were SP Initiated
          passed_data['destination'] = destination
          passed_data['in_response_to'] = "IdP_Initiated_Login"
  
          # Construct SamlResponse messages
          authn_resp = build_authn_response(request.user, get_authn(), passed_data, service_provider)
  
          html_response = self.create_html_response(request, binding_out, authn_resp, destination, passed_data.get('RelayState', ""))
          return self.render_response(request, html_response, processor)
  
  
  @method_decorator(never_cache, name='dispatch')
  class ProcessMultiFactorView(LoginRequiredMixin, View):
      """ This view is used in an optional step is to perform 'other' user validation, for example 2nd factor checks.
          Override this view per the documentation if using this functionality to plug in your custom validation logic.
      """
  
      def multifactor_is_valid(self, request: HttpRequest) -> bool:
          """ The code here can do whatever it needs to validate your user (via request.user or elsewise).
              It must return True for authentication to be considered a success.
          """
          return True
  
      def get(self, request: HttpRequest, *args, **kwargs):
          if self.multifactor_is_valid(request):
              logger.debug('MultiFactor succeeded for %s' % request.user)
              html_response = request.session['saml_data']
              if html_response['type'] == 'POST':
                  return HttpResponse(html_response['data'])
              else:
                  return HttpResponseRedirect(html_response['data'])
          logger.debug(_("MultiFactor failed; %s will not be able to log in") % request.user)
          logout(request)
          raise PermissionDenied(_("MultiFactor authentication factor failed"))
  
  
  @method_decorator([never_cache, csrf_exempt], name='dispatch')
  class LogoutProcessView(LoginRequiredMixin, IdPHandlerViewMixin, View):
      """ View which processes the actual SAML Single Logout request
          The login_required decorator ensures the user authenticates first on the IdP using 'normal' way.
      """
      __service_name = 'Single LogOut'
  
      def post(self, request: HttpRequest, *args, **kwargs):
          return self.get(request, *args, **kwargs)
  
      def get(self, request: HttpRequest, *args, **kwargs):
          logger.info("--- {} Service ---".format(self.__service_name))
          # do not assign a variable that overwrite request object, if it will fail the return with HttpResponseBadRequest trows naturally
          store_params_in_session(request)
          binding = request.session['Binding']
          relay_state = request.session['RelayState']
          logger.debug("--- {} requested [\n{}] to IDP ---".format(self.__service_name, binding))
  
          idp_server = IDP.load()
  
          # adapted from pysaml2 examples/idp2/idp_uwsgi.py
          try:
              req_info = idp_server.parse_logout_request(request.session['SAMLRequest'], binding)
          except Exception as excp:
              expc_msg = "{} Bad request: {}".format(self.__service_name, excp)
              logger.error(expc_msg)
              return error_cbv.handle_error(request, exception=expc_msg, status_code=400)
  
          logger.debug("{} - local identifier: {} from {}".format(self.__service_name, req_info.message.name_id.text, req_info.message.name_id.sp_name_qualifier))
          logger.debug("--- {} SAML request [\n{}] ---".format(self.__service_name, repr_saml(req_info.xmlstr, b64=False)))
  
          # TODO
          # check SAML request signature
          try:
              verify_request_signature(req_info)
          except ValueError as excp:
              return error_cbv.handle_error(request, exception=excp, status_code=400)
  
          resp = idp_server.create_logout_response(req_info.message, [binding])
  
          '''
          # TODO: SOAP
          # if binding == BINDING_SOAP:
              # destination = ""
              # response = False
          # else:
              # binding, destination = IDP.pick_binding(
                  # "single_logout_service", [binding], "spsso", req_info
              # )
              # response = True
          # END TODO SOAP'''
  
          try:
              # hinfo returns request or response, it depends by request arg
              hinfo = idp_server.apply_binding(binding, resp.__str__(), resp.destination, relay_state, response=True)
          except Exception as excp:
              logger.error("ServiceError: %s", excp)
              return error_cbv.handle_error(request, exception=excp, status=400)
  
          logger.debug("--- {} Response [\n{}] ---".format(self.__service_name, repr_saml(resp.__str__().encode())))
          logger.debug("--- binding: {} destination:{} relay_state:{} ---".format(binding, resp.destination, relay_state))
  
          # TODO: double check username session and saml login request
          # logout user from IDP
          logout(request)
  
          if hinfo['method'] == 'GET':
              return HttpResponseRedirect(hinfo['headers'][0][1])
          else:
              html_response = self.create_html_response(
                  request,
                  binding=binding,
                  authn_resp=resp.__str__(),
                  destination=resp.destination,
                  relay_state=relay_state)
          return self.render_response(request, html_response, None)
  
  
  @never_cache
  def get_multifactor(request: HttpRequest) -> HttpResponse:
      if hasattr(settings, "SAML_IDP_MULTIFACTOR_VIEW"):
          multifactor_class = import_string(getattr(settings, "SAML_IDP_MULTIFACTOR_VIEW"))
      else:
          multifactor_class = ProcessMultiFactorView
      return multifactor_class.as_view()(request)
  
  
  @never_cache
  def metadata(request: HttpRequest) -> HttpResponse:
      """ Returns an XML with the SAML 2.0 metadata for this Idp.
          The metadata is constructed on-the-fly based on the config dict in the django settings.
      """
      return HttpResponse(content=IDP.metadata().encode('utf-8'), content_type="text/xml; charset=utf8")
  