# authentic2 - versatile identity manager
# Copyright (C) 2010-2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""SAML2.0 IdP implementation

   It contains endpoints to receive:
    - authentication requests,
    - logout request,
    - logout response,
    - name id management requests,
    - name id management responses,
    - attribut requests.
    - logout
    - logoutResponse

    TODO:
     - manageNameId
     - manageNameIdResponse
     - assertionIDRequest
"""


import datetime
import hashlib
import logging
import random
import string
import xml.etree.ElementTree as ctree
from functools import wraps
from urllib.parse import quote, urlencode

from django.conf import settings
from django.contrib import messages
from django.contrib.auth import BACKEND_SESSION_KEY, REDIRECT_FIELD_NAME, get_user_model, load_backend
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.encoding import force_bytes, force_str, force_text
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_noop as N_
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

import authentic2.saml.saml2utils as saml2utils
import authentic2.views as a2_views
from authentic2 import hooks
from authentic2.attributes_ng.engine import get_attributes
from authentic2.compat_lasso import lasso
from authentic2.constants import NONCE_FIELD_NAME
from authentic2.idp import signals as idp_signals
from authentic2.idp.saml.common import kill_django_sessions
from authentic2.saml.common import (
    AUTHENTIC_SAME_ID_SENTINEL,
    AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR,
    AUTHENTIC_STATUS_CODE_MISSING_DESTINATION,
    AUTHENTIC_STATUS_CODE_MISSING_NAMEID,
    AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX,
    AUTHENTIC_STATUS_CODE_UNAUTHORIZED,
    AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER,
    AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION,
    asynchronous_bindings,
    create_saml2_server,
    error_page,
    get_entity_id,
    get_saml2_metadata,
    get_saml2_query_request,
    get_saml2_request_message,
    get_saml2_request_message_async_binding,
    get_soap_message,
    get_sp_options_policy,
    load_federation,
    load_provider,
    redirect_next,
    return_saml2_response,
    return_saml_soap_response,
    send_soap_request,
    set_saml2_response_responder_status_code,
    soap_bindings,
    soap_fault,
)
from authentic2.saml.models import (
    NAME_ID_FORMATS,
    LibertyArtifact,
    LibertyFederation,
    LibertyProvider,
    LibertyServiceProvider,
    LibertySession,
    SAMLAttribute,
    get_and_delete_key_values,
    nameid2kwargs,
    nidformat_to_saml2_urn,
    saml2_urn_to_nidformat,
    save_key_values,
)
from authentic2.utils import misc as utils_misc
from authentic2.utils.misc import datetime_to_xs_datetime, find_authentication_event
from authentic2.utils.misc import get_backends as get_idp_backends
from authentic2.utils.misc import login_require, make_url

from . import app_settings

User = get_user_model()

logger = logging.getLogger(__name__)


def get_nonce():
    alphabet = string.ascii_letters + string.digits
    return '_' + ''.join(random.SystemRandom().choice(alphabet) for i in range(20))


metadata_map = (
    (saml2utils.Saml2Metadata.SINGLE_SIGN_ON_SERVICE, asynchronous_bindings, '/sso'),
    (saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE, asynchronous_bindings, '/slo', '/slo_return'),
    (saml2utils.Saml2Metadata.SINGLE_LOGOUT_SERVICE, soap_bindings, '/slo/soap'),
    (saml2utils.Saml2Metadata.ARTIFACT_RESOLUTION_SERVICE, lasso.SAML2_METADATA_BINDING_SOAP, '/artifact'),
)


def metadata(request):
    '''Endpoint to retrieve the metadata file'''
    options = get_options()
    metadata = get_saml2_metadata(request, idp_map=metadata_map, options=options)
    return HttpResponse(metadata, content_type='text/xml')


def log_assert(func, exception_classes=(AssertionError,)):
    """Convert assertion errors to warning logs and report them to the user
    through the messages framework.

    Returns a redirect to homepage or the `next` query parameter.
    """

    @wraps(func)
    def f(request, *args, **kwargs):
        try:
            return func(request, *args, **kwargs)
        except exception_classes as e:
            return error_redirect(request, str(e))

    return f


#####
# SSO
#####


def register_new_saml2_session(request, login):
    '''Persist the newly created session for emitted assertion'''
    lib_session = LibertySession(
        provider_id=login.remoteProviderId,
        saml2_assertion=login.assertion,
        django_session_key=request.session.session_key,
    )
    lib_session.save()


def fill_assertion(request, saml_request, assertion, provider_id, nid_format):
    """Stuff an assertion with information extracted from the user record
       and from the session, and eventually from transactions linked to the
       request, i.e. a login event or a consent event.

       No check on the request must be done here, the sso method should have
       verified that the request can be answered and match any policy for the
       given provider or modified the request to match the identity provider
       policy.

    TODO: determine and add attributes from the session, for anonymous users
    (pseudonymous federation)
    # TODO: add information from the login event, of the session or linked
    # to the request id
    # TODO: use information from the consent event to specialize release of
    # attributes (user only authorized to give its email for email)
    """
    assert nid_format in NAME_ID_FORMATS

    logger.debug('initializing assertion %s', assertion.id)
    # Use assertion ID as session index
    assertion.authnStatement[0].sessionIndex = assertion.id
    logger.debug('nid_format is %s', nid_format)
    if nid_format == 'edupersontargetedid':
        nid_format = 'transient'
    if nid_format == 'transient':
        # Generate the transient identifier from the session key, to fix it for
        # a session duration, without that logout is broken as you can send
        # many session_index in a logout request but only one NameID
        keys = ''.join([request.session.session_key, provider_id, settings.SECRET_KEY])
        transient_id_content = '_' + hashlib.sha1(force_bytes(keys)).hexdigest().upper()
        assertion.subject.nameID.content = transient_id_content
    if nid_format == 'email':
        assert request.user.email, 'email is required when using the email NameID format'
        assertion.subject.nameID.content = request.user.email
    if nid_format == 'username':
        assert request.user.username, 'username field is required when using the username NameID format'
        assertion.subject.nameID.content = force_str(request.user.username)
    if nid_format == 'uuid':
        assertion.subject.nameID.content = request.user.uuid
    assertion.subject.nameID.format = NAME_ID_FORMATS[nid_format]['samlv2']


def get_attribute_definitions(provider):
    '''Query all attribute definitions for a providers'''
    qs = SAMLAttribute.objects.for_generic_object(provider).filter(enabled=True)
    sp_options_policy = get_sp_options_policy(provider)
    if sp_options_policy:
        qs |= SAMLAttribute.objects.for_generic_object(sp_options_policy).filter(enabled=True)
    return qs.distinct()


def make_edu_person_targeted_id_value(provider, user):
    salt = app_settings.EDU_PERSON_TARGETED_ID_SALT
    attribute = app_settings.EDU_PERSON_TARGETED_ID_ATTRIBUTE

    if not attribute or not salt:
        return None

    if hasattr(user.attributes, attribute):
        attribute_value = getattr(user.attributes, attribute, None)
    else:
        attribute_value = getattr(user, attribute, None)
    if attribute_value is None:
        return None

    keys = [force_bytes(salt), force_bytes(provider.entity_id), force_bytes(attribute_value)]
    return '_' + hashlib.sha256(b''.join(keys)).hexdigest().upper()


def make_edu_person_targeted_id(entity_id, provider, user):
    value = make_edu_person_targeted_id_value(provider, user)
    if not value:
        return None

    name_id = lasso.Saml2NameID()
    name_id.format = lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT
    name_id.nameQualifier = force_str(entity_id)
    name_id.spNameQualifier = force_str(provider.entity_id)
    name_id.content = force_str(value)
    return ctree.fromstring(name_id.exportToXml())


def add_attributes(request, entity_id, assertion, provider, nid_format):
    qs = get_attribute_definitions(provider)
    wanted_attributes = [definition.attribute_name for definition in qs]

    edu_person_targeted_id = make_edu_person_targeted_id(entity_id, provider, request.user)

    if not assertion.attributeStatement:
        assertion.attributeStatement = [lasso.Saml2AttributeStatement()]
    attribute_statement = assertion.attributeStatement[0]
    attributes = {}
    seen = set()
    # Keep current attributes, mark string values as already added
    for attribute in attribute_statement.attribute:
        name = force_text(attribute.name)
        name_format = force_text(attribute.nameFormat)
        # sequence from lasso are always tuple, so convert to list
        attributes[(name, name_format)] = attribute, list(attribute.attributeValue)
        for atv in attribute.attributeValue:
            if (
                atv.any
                and len(atv.any) == 1
                and isinstance(atv.any[0], lasso.MiscTextNode)
                and atv.any[0].textChild
            ):
                seen.add((name, name_format, force_text(atv.any[0].content)))
    definitions = list(qs)

    # special handling of nid format edupersontargetedid
    edu_name = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'
    if nid_format == 'edupersontargetedid':
        if (edu_name, lasso.SAML2_ATTRIBUTE_NAME_FORMAT_URI) not in attributes:
            definitions.append(
                SAMLAttribute(
                    name=edu_name,
                    name_format='uri',
                    attribute_name='edupersontargetedid',
                    friendly_name='eduPersonTargetedID',
                )
            )

    for definition in definitions:
        name = definition.name
        name_format = definition.name_format_uri()
        friendly_name = definition.friendly_name
        if (name, name_format) in attributes:
            continue
        attribute, value = attributes[(name, name_format)] = lasso.Saml2Attribute(), []
        attribute.friendlyName = force_str(friendly_name)
        attribute.name = force_str(name)
        attribute.nameFormat = force_str(name_format)
    verified = set()

    ctx = {
        'request': request,
        'user': request.user,
        'service': provider,
        '__wanted_attributes': wanted_attributes,
    }
    if edu_person_targeted_id is not None:
        ctx['edupersontargetedid'] = [edu_person_targeted_id]
    attributes_ctx = get_attributes(ctx)

    for definition in qs:
        verified_attribute_name = definition.attribute_name + ':verified'
        if attributes_ctx.get(verified_attribute_name):
            verified.add(definition.name)
    attributes_ctx['@verified_attributes@'] = list(verified)

    tuples = [tuple(t) for definition in definitions for t in definition.to_tuples(attributes_ctx)]

    seen = set()
    for name, name_format, friendly_name, value in tuples:
        # prevent repeating attribute values
        if (name, name_format, value) in seen:
            continue
        seen.add((name, name_format, value))
        attribute, values = attributes[(name, name_format)]

        # We keep only one friendly name
        if not attribute.friendlyName and friendly_name:
            attribute.friendlyName = force_str(friendly_name)
        tn = value_to_misc_text_node(value)
        if tn:
            atv = lasso.Saml2AttributeValue()
            atv.any = [tn]
            values.append(atv)
    for attribute, values in attributes.values():
        attribute.attributeValue = values
    attribute_statement.attribute = [attribute for attribute, values in attributes.values()]


def value_to_misc_text_node(value):
    if hasattr(value, 'makeelement') and hasattr(value, 'tag'):
        return lasso.MiscTextNode.newWithXmlNode(force_str(ctree.tostring(value)))
    else:
        if value is True:
            value = 'true'
        elif value is False:
            value = 'false'
        elif value is None:
            value = ''
        else:
            value = force_text(value)
        tn = lasso.MiscTextNode.newWithString(force_str(value))
        tn.textChild = True
        return tn


def saml2_add_attribute_values(assertion, attributes):
    if attributes:
        logger.debug('adding attributes')
        logger.debug('assertion before processing %s', force_text(assertion.dump()))
        logger.debug('adding attributes %s', attributes)
        if not assertion.attributeStatement:
            assertion.attributeStatement = [lasso.Saml2AttributeStatement()]
        attribute_statement = assertion.attributeStatement[0]
        for key in attributes.keys():
            attribute = lasso.Saml2Attribute()
            # Only name/values or name/format/values
            name = None
            values = None
            if type(key) is tuple and len(key) == 2:
                name, format = key
                attribute.nameFormat = format
                values = attributes[(name, format)]
            elif type(key) is tuple and len(key) == 3:
                name, format, nickname = key
                attribute.nameFormat = format
                attribute.friendlyName = nickname
                values = attributes[(name, format, nickname)]
            elif type(key) is tuple:
                return
            else:
                name = key
                attribute.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
                values = attributes[key]
            attribute.name = name
            attribute_statement.attribute = list(attribute_statement.attribute) + [attribute]
            attribute_value_list = list(attribute.attributeValue)
            for value in values:
                text_node = value_to_misc_text_node(value)
                if text_node:
                    attribute_value = lasso.Saml2AttributeValue()
                    attribute_value.any = [text_node]
                    attribute_value_list.append(attribute_value)
            attribute.attributeValue = attribute_value_list
        logger.debug('assertion after processing %s', force_text(assertion.dump()))


def build_assertion(request, login, provider, nid_format='transient'):
    """After a successfully validated authentication request, build an
    authentication assertion
    """
    entity_id = get_entity_id(request)
    now = datetime.datetime.utcnow()
    logger.debug('NameIDFormat is %s', nid_format)
    # 1 minute ago
    notBefore = now - datetime.timedelta(0, app_settings.SECONDS_TOLERANCE)
    # 1 minute in the future
    notOnOrAfter = now + datetime.timedelta(0, app_settings.SECONDS_TOLERANCE)
    ssl = 'HTTPS' in request.environ
    if app_settings.AUTHN_CONTEXT_FROM_SESSION:
        backend = request.session[BACKEND_SESSION_KEY]
        logger.debug('authentication from session with backend %s', backend)
        backend = load_backend(backend)
        if hasattr(backend, 'get_saml2_authn_context'):
            authn_context = backend.get_saml2_authn_context()
        else:
            raise Exception('backend unsupported: ' + backend)
        if authn_context == lasso.SAML2_AUTHN_CONTEXT_PASSWORD and ssl:
            authn_context = lasso.SAML2_AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT
    else:
        try:
            event = find_authentication_event(request, login.request.id)
            logger.debug('authentication from stored event %s', event)
            how = event['how']
            if how == 'password':
                authn_context = lasso.SAML2_AUTHN_CONTEXT_PASSWORD
            elif how == 'password-on-https':
                authn_context = lasso.SAML2_AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT
            elif how.startswith('oath-totp'):
                authn_context = lasso.SAML2_AUTHN_CONTEXT_TIME_SYNC_TOKEN
            else:
                raise NotImplementedError('Unknown authentication method %s', how)
        except ObjectDoesNotExist:
            # TODO: previous session over secure transport (ssl) ?
            authn_context = lasso.SAML2_AUTHN_CONTEXT_PREVIOUS_SESSION
    logger.debug('authn_context is %s', authn_context)
    login.buildAssertion(
        authn_context,
        now.isoformat() + 'Z',
        'unused',  # reauthenticateOnOrAfter is only for ID-FF 1.2
        notBefore.isoformat() + 'Z',
        notOnOrAfter.isoformat() + 'Z',
    )
    assertion = login.assertion
    assertion.conditions.notOnOrAfter = notOnOrAfter.isoformat() + 'Z'
    # Set SessionNotOnOrAfter to expiry date of the current session, so we are sure no session on
    # service providers can outlive the IdP session.
    expiry_date = request.session.get_expiry_date()
    assertion.authnStatement[0].sessionNotOnOrAfter = datetime_to_xs_datetime(expiry_date)
    logger.debug('assertion building in progress %s', force_text(assertion.dump()))
    fill_assertion(request, login.request, assertion, login.remoteProviderId, nid_format)
    # Save federation and new session
    if nid_format == 'persistent':
        logger.debug('nameID persistent, get or create federation')
        kwargs = nameid2kwargs(login.assertion.subject.nameID)
        # if qualifiers can be inferred from providers entityID replace them by
        # placeholders
        if kwargs.get('name_id_qualifier') == entity_id:
            kwargs['name_id_qualifier'] = AUTHENTIC_SAME_ID_SENTINEL
        if kwargs.get('name_id_sp_name_qualifier') == login.remoteProviderId:
            kwargs['name_id_sp_name_qualifier'] = AUTHENTIC_SAME_ID_SENTINEL
        service_provider = LibertyServiceProvider.objects.get(
            liberty_provider__entity_id=login.remoteProviderId
        )
        federation, new = LibertyFederation.objects.get_or_create(
            sp=service_provider, user=request.user, **kwargs
        )
        if new:
            logger.debug('nameID persistent, new federation')
            federation.save()
        else:
            logger.debug('nameID persistent, existing federation')
    else:
        logger.debug('nameID not persistent, no federation management')
        federation = None
        kwargs = nameid2kwargs(login.assertion.subject.nameID)
    kwargs['entity_id'] = login.remoteProviderId
    kwargs['user'] = request.user
    logger.debug(
        'sending nameID %(name_id_format)r: %(name_id_content)r to '
        '%(entity_id)s for user %(user)s' % kwargs
    )
    register_new_saml2_session(request, login)
    add_attributes(request, entity_id, assertion, provider, nid_format)
    return kwargs['name_id_content']


@never_cache
@csrf_exempt
@log_assert
def sso(request):
    """Endpoint for receiving saml2:AuthnRequests by POST, Redirect or SOAP.
    For SOAP a session must be established previously through the login
    page. No authentication through the SOAP request is supported.
    """
    if request.method == "GET":
        logger.debug('called by GET')
        consent_answer = request.GET.get('consent_answer', '')
        if consent_answer:
            logger.debug('back from the consent page for federation with answer %s', consent_answer)
    server = create_server(request)
    login = lasso.Login(server)
    message = get_saml2_request_message(request, login)
    # 1. Process the request, separate POST and GET treatment
    if not message:
        return HttpResponseForbidden(
            'A SAMLv2 Single Sign On request need a query string', content_type='text/plain'
        )
    logger.debug('processing sso request %r', message)
    policy = None
    signed = True
    while True:
        try:
            login.processAuthnRequestMsg(force_str(message))
            # keep a copy of authnRequest in the session, it may later be used
            # in hooks or plugins to look into Extensions, for example.
            request.session['saml:authnRequest'] = login.request.getOriginalXmlnode()
            break
        except (lasso.ProfileInvalidMsgError, lasso.ProfileMissingIssuerError) as e:
            logger.warning(
                'invalid message for WebSSO profile with HTTP-Redirect binding: ' '%r exception: %s',
                message,
                e,
                extra={'request': request},
            )
            return HttpResponseBadRequest(
                _("SAMLv2 Single Sign On: invalid message for WebSSO profile with HTTP-Redirect binding"),
                content_type='text/plain',
            )
        except lasso.ProfileInvalidProtocolprofileError:
            log_info_authn_request_details(login)
            message = _(
                "SAMLv2 Single Sign On: the request cannot be "
                "answered because no valid protocol binding could be found"
            )
            logger.warning('the request cannot be answered because no valid protocol binding could be found')
            return HttpResponseBadRequest(message, content_type='text/plain')
        except lasso.ProviderMissingPublicKeyError as e:
            log_info_authn_request_details(login)
            logger.warning('no public key found: %s', e)
            login.response.status.statusMessage = 'The public key is unknown'
            return return_login_response(request, login)
        except lasso.DsError as e:
            log_info_authn_request_details(login)
            logger.warning('digital signature treatment error: %s', e)
            login.response.status.statusMessage = 'Signature validation failed'
            return return_login_response(request, login)
        except (lasso.ServerProviderNotFoundError, lasso.ProfileUnknownProviderError):
            logger.debug('processAuthnRequestMsg not successful')
            log_info_authn_request_details(login)
            provider_id = login.remoteProviderId
            logger.debug('loading provider %s' % provider_id)
            provider_loaded = load_provider(request, provider_id, server=login.server, autoload=True)
            if not provider_loaded:
                add_url = reverse('admin:saml_libertyprovider_add_from_url')
                add_url += '?' + urlencode({'entity_id': provider_id})
                return render(
                    request,
                    'idp/saml/unknown_provider.html',
                    {
                        'entity_id': provider_id,
                        'add_url': add_url,
                    },
                )
            else:
                policy = get_sp_options_policy(provider_loaded)
                if not policy:
                    return error_page(request, _('sso: No SP policy defined'), logger=logger, warning=True)
                logger.debug('provider %s loaded with success', provider_id)
            if policy.authn_request_signed:
                verify_hint = lasso.PROFILE_SIGNATURE_VERIFY_HINT_FORCE
            else:
                verify_hint = lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE
                signed = False
            login.setSignatureVerifyHint(verify_hint)

    if signed and not check_destination(request, login.request):
        logger.warning('wrong or absent destination')
        return return_login_error(request, login, AUTHENTIC_STATUS_CODE_MISSING_DESTINATION)
    # Check NameIDPolicy or force the NameIDPolicy
    name_id_policy = login.request.nameIdPolicy
    if (
        name_id_policy
        and name_id_policy.format
        and name_id_policy.format != lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED
    ):
        logger.debug('nameID policy is %s', force_text(name_id_policy.dump()))
        nid_format = saml2_urn_to_nidformat(name_id_policy.format, accepted=policy.accepted_name_id_format)
        logger.debug('nameID format %s', nid_format)
        default_nid_format = policy.default_name_id_format
        logger.debug('default nameID format %s', default_nid_format)
        accepted_nid_format = policy.accepted_name_id_format
        logger.debug('nameID format accepted %s', accepted_nid_format)
        if (not nid_format or nid_format not in accepted_nid_format) and default_nid_format != nid_format:
            set_saml2_response_responder_status_code(
                login.response, lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY
            )
            logger.warning('NameID format required is not accepted')
            return finish_sso(request, login)
    else:
        logger.debug('no nameID policy format')
        nid_format = policy.default_name_id_format or 'transient'
        if not name_id_policy:
            logger.debug('no nameID policy at all')
            login.request.nameIdPolicy = lasso.Samlp2NameIDPolicy()
            name_id_policy = login.request.nameIdPolicy
        name_id_policy.format = NAME_ID_FORMATS[nid_format]['samlv2']
        logger.debug('set nameID policy format %s', nid_format)
    return sso_after_process_request(request, login, nid_format=nid_format)


def need_login(request, login, nid_format, service):
    """Redirect to the login page with a nonce parameter to verify later that
    the login form was submitted
    """
    nonce = login.request.id or get_nonce()
    save_key_values(nonce, force_text(login.dump()), False, nid_format)
    next_url = make_url(continue_sso, params={NONCE_FIELD_NAME: nonce})
    logger.debug('redirect to login page with next url %s', next_url)
    return login_require(
        request,
        next_url=next_url,
        params={NONCE_FIELD_NAME: nonce},
        service=service,
        login_hint=get_login_hints_extension(login),
    )


def get_url_with_nonce(request, function, nonce):
    url = reverse(function) + '?%s=%s' % (NONCE_FIELD_NAME, nonce)
    return quote(url)


def need_consent_for_federation(request, login, nid_format):
    nonce = login.request.id or get_nonce()
    save_key_values(nonce, force_text(login.dump()), False, nid_format)
    display_name = None
    try:
        provider = LibertyProvider.objects.get(entity_id=login.request.issuer.content)
        display_name = provider.name
    except ObjectDoesNotExist:
        pass
    if not display_name:
        display_name = quote(login.request.issuer.content)
    url = '%s?%s=%s&next=%s&provider_id=%s' % (
        reverse('a2-consent-federation'),
        NONCE_FIELD_NAME,
        nonce,
        get_url_with_nonce(request, continue_sso, nonce),
        display_name,
    )
    logger.debug('redirect to url %s', url)
    return HttpResponseRedirect(url)


@never_cache
def continue_sso(request):
    consent_answer = None
    consent_attribute_answer = None
    if request.method == "GET":
        logger.debug('called by GET')
        consent_answer = request.GET.get('consent_answer', '')
        if consent_answer:
            logger.debug('back from the consent page for federation with answer %s', consent_answer)
        consent_attribute_answer = request.GET.get('consent_attribute_answer', '')
        if consent_attribute_answer:
            logger.debug('back from the consent page for attributes %s', consent_attribute_answer)
    nonce = request.GET.get(NONCE_FIELD_NAME, '')
    if not nonce:
        logger.warning('nonce not found')
        return HttpResponseBadRequest()
    try:
        login_dump, consent_obtained, nid_format = get_and_delete_key_values(nonce)
    except KeyError:
        messages.warning(request, _('request has expired'))
        return utils_misc.redirect(request, 'auth_homepage')
    server = create_server(request)
    # Work Around for lasso < 2.3.6
    login_dump = login_dump.replace('<Login ', '<lasso:Login ').replace('</Login>', '</lasso:Login>')
    login = lasso.Login.newFromDump(server, force_str(login_dump))
    logger.debug('login newFromDump done')
    if not login:
        return error_page(request, _('continue_sso: error loading login'), logger=logger)
    if not load_provider(request, login.remoteProviderId, server=login.server, autoload=True):
        return error_page(
            request, _('continue_sso: unknown provider %s') % login.remoteProviderId, logger=logger
        )
    if 'cancel' in request.GET:
        logger.debug('login canceled')
        set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
        return finish_sso(request, login)
    if consent_answer == 'refused':
        logger.debug('consent answer treatment, the user refused, return request denied to the requester')
        set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
        return finish_sso(request, login)
    if consent_answer == 'accepted':
        logger.debug('consent answer treatment, the user accepted, continue')
        consent_obtained = True
    return sso_after_process_request(
        request,
        login,
        consent_obtained=consent_obtained,
        consent_attribute_answer=consent_attribute_answer,
        nid_format=nid_format,
    )


def needs_persistence(nid_format):
    return nid_format not in ['transient', 'email', 'username', 'edupersontargetedid']


def get_extensions(profile):
    if not profile.request.extensions:
        return []
    xml_string = force_bytes(profile.request.extensions.dump())
    element = ctree.fromstring(xml_string)
    return list(element)


EO_NS = 'https://www.entrouvert.com/'
LOGIN_HINT = '{%s}login-hint' % EO_NS


def get_login_hints_extension(profile):
    hints = set()
    for node in get_extensions(profile):
        if node.tag != LOGIN_HINT:
            continue
        if len(node) != 0:
            continue  # no children accepted
        for hint in node.text.split():
            if hint:
                hints.add(hint)
    return hints


def sso_after_process_request(
    request,
    login,
    consent_obtained=False,
    consent_attribute_answer=False,
    user=None,
    nid_format='transient',
    return_profile=False,
):
    """Common path for sso and idp_initiated_sso.

    consent_obtained: whether the user has given his consent to this
    federation
    user: the user which must be federated, if None, current user is the
    default.
    """
    nonce = login.request.id
    user = user or request.user
    did_auth = find_authentication_event(request, nonce) is not None
    force_authn = login.request.forceAuthn
    passive = login.request.isPassive

    logger.debug('NameIDFormat is %s', nid_format)
    logger.debug('nonce is %s', nonce)

    # check if user is authorized through this service
    service = LibertyServiceProvider.objects.get(
        liberty_provider__entity_id=login.remoteProviderId
    ).liberty_provider

    if not passive and (user.is_anonymous or (force_authn and not did_auth)):
        logger.debug('login required')
        return need_login(request, login, nid_format, service)

    # No user is authenticated and passive is True, deny request
    if passive and user.is_anonymous:
        logger.debug('no user connected and passive request, returning NoPassive')
        set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_NO_PASSIVE)
        return finish_sso(request, login)

    service.authorize(request.user)

    hooks.call_hooks('event', name='sso-request', idp='saml2', service=service)

    # Do not ask consent for federation if a transient nameID is provided
    transient = False
    if nid_format == 'transient':
        transient = True

    decisions = idp_signals.authorize_service.send(
        sender=None, request=request, user=request.user, audience=login.remoteProviderId, attributes={}
    )
    logger.debug('signal authorize_service sent')

    # You don't dream. By default, access granted.
    # We catch denied decisions i.e. dic['authz'] = False
    access_granted = True
    for decision in decisions:
        logger.debug('authorize_service connected to function %s' % decision[0].__name__)
        dic = decision[1]
        if dic and 'authz' in dic:
            logger.debug('decision is %s', dic['authz'])
            if 'message' in dic:
                logger.debug('with message %s', dic['message'])
            if not dic['authz']:
                logger.debug('access denied by an external function')
                access_granted = False
        else:
            logger.debug('no function connected to authorize_service')

    if not access_granted:
        logger.debug('access denied, return answer to the requester')
        set_saml2_response_responder_status_code(
            login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED, msg=str(dic['message'])
        )
        return finish_sso(request, login)

    provider = load_provider(request, login.remoteProviderId, server=login.server)
    if not provider:
        return error_page(request, _('Provider %s is unknown') % login.remoteProviderId, logger=logger)
    saml_policy = get_sp_options_policy(provider)
    if not saml_policy:
        return error_page(request, _('No service provider policy defined'), logger=logger)

    '''User consent for federation management

       1- Check if the policy enforce the consent
       2- Check if there is an existing federation (consent already given)
       3- If no, send a signal to bypass consent
       4- If no bypass captured, ask for the user consent
       5- Yes, continue, No, return error to the service provider

    From the core SAML2 specs.

        'urn:oasis:names:tc:SAML:2.0:consent:unspecified'
    No claim as to principal consent is being made.

        'urn:oasis:names:tc:SAML:2.0:consent:obtained'
    Indicates that a principal's consent has been obtained by the issuer of
    the message.

        'urn:oasis:names:tc:SAML:2.0:consent:prior'
    Indicates that a principal's consent has been obtained by the issuer of
    the message at some point prior to the action that initiated the message.

        'urn:oasis:names:tc:SAML:2.0:consent:current-implicit'
    Indicates that a principal's consent has been implicitly obtained by the
    issuer of the message during the action that initiated the message, as
    part of a broader indication of consent. Implicit consent is typically
    more proximal to the action in time and presentation than prior consent,
    such as part of a session of activities.

        'urn:oasis:names:tc:SAML:2.0:consent:current-explicit'
    Indicates that a principal's consent has been explicitly obtained by the
    issuer of the message during the action that initiated the message.

        'urn:oasis:names:tc:SAML:2.0:consent:unavailable'
    Indicates that the issuer of the message did not obtain consent.

        'urn:oasis:names:tc:SAML:2.0:consent:inapplicable'
    Indicates that the issuer of the message does not believe that they need
    to obtain or report consent
    '''

    logger.debug('the user consent status before process is %s', consent_obtained)

    consent_value = None
    if consent_obtained:
        consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:current-explicit'
    else:
        consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:unavailable'

    if not consent_obtained and not transient:
        consent_obtained = not saml_policy.ask_user_consent
        logger.debug('the policy says %s', consent_obtained)
        if consent_obtained:
            # The user consent is bypassed by the policy
            consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:unspecified'

    if needs_persistence(nid_format):
        try:
            LibertyFederation.objects.get(
                user=request.user, sp__liberty_provider__entity_id=login.remoteProviderId
            )
            logger.debug('consent already given (existing federation) for %s', login.remoteProviderId)
            consent_obtained = True
            '''This is abusive since a federation may exist even if we have
            not previously asked the user consent.'''
            consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:prior'
        except ObjectDoesNotExist:
            logger.debug('consent not yet given (no existing federation) for %s', login.remoteProviderId)

    if not consent_obtained and not transient:
        logger.debug('signal avoid_consent sent')
        avoid_consent = idp_signals.avoid_consent.send(
            sender=None, request=request, user=request.user, audience=login.remoteProviderId
        )
        for c in avoid_consent:
            logger.debug('avoid_consent connected to function %s', c[0].__name__)
            if c[1] and 'avoid_consent' in c[1] and c[1]['avoid_consent']:
                logger.debug('avoid consent by signal')
                consent_obtained = True
                # The user consent is bypassed by the signal
                consent_value = 'urn:oasis:names:tc:SAML:2.0:consent:unspecified'

    if not consent_obtained and not transient:
        logger.debug('ask the user consent now')
        return need_consent_for_federation(request, login, nid_format)

    logger.debug('login dump before processing %s', force_text(login.dump()))
    try:
        if needs_persistence(nid_format):
            logger.debug('load identity dump')
            load_federation(request, get_entity_id(request), login, user)
        login.validateRequestMsg(not user.is_anonymous, consent_obtained)
        logger.debug('validateRequestMsg %s', force_text(login.dump()))
    except lasso.LoginRequestDeniedError:
        logger.warning('access denied due to LoginRequestDeniedError')
        set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
        return finish_sso(request, login, user=user)
    except lasso.LoginFederationNotFoundError:
        logger.warning('access denied due to LoginFederationNotFoundError')
        set_saml2_response_responder_status_code(login.response, lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
        return finish_sso(request, login, user=user)

    login.response.consent = consent_value

    name_id = build_assertion(request, login, provider, nid_format=nid_format)
    hooks.call_hooks('event', name='sso-success', idp='saml2', service=service, user=request.user)
    logger.info('sso success sending AuthenticationResponse to %s with NameID %s', service, name_id)
    return finish_sso(request, login, user=user, return_profile=return_profile)


def return_login_error(request, login, error):
    """Set the first level status code to Responder, the second level to error
    and return the response message for the assertionConsumer"""
    logger.debug('return_login_error %s', error)
    set_saml2_response_responder_status_code(login.response, error)
    return return_login_response(request, login)


def return_login_response(request, login):
    '''Return the AuthnResponse message to the assertion consumer'''
    if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
        login.buildArtifactMsg(lasso.HTTP_METHOD_ARTIFACT_GET)
        logger.debug('sending Artifact to assertionConsumer %s', login.msgUrl)
        save_artifact(request, login)
    elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
        login.buildAuthnResponseMsg()
        logger.debug('sending POST to assertionConsumer %s', login.msgUrl)
        logger.debug('POST content %s', login.msgBody)
    else:
        raise NotImplementedError()
    provider = LibertyProvider.objects.get(entity_id=login.remoteProviderId)
    return return_saml2_response(request, login, title=_('You are being redirected to "%s"') % provider.name)


def finish_sso(request, login, user=None, return_profile=False):
    if user is None:
        user = request.user
    response = return_login_response(request, login)
    if return_profile:
        return login
    return response


def save_artifact(request, login):
    '''Remember an artifact message for later retrieving'''
    LibertyArtifact(
        artifact=login.artifact, content=force_text(login.artifactMessage), provider_id=login.remoteProviderId
    ).save()
    logger.debug('artifact saved')


def reload_artifact(login):
    try:
        art = LibertyArtifact.objects.get(artifact=login.artifact)
        login.artifactMessage = force_str(art.content)
        art.delete()
        logger.debug('artifact found')
    except ObjectDoesNotExist:
        logger.debug('no artifact found')
        pass


@require_POST
@never_cache
@csrf_exempt
def artifact(request):
    """Resolve a SAMLv2 ArtifactResolve request"""
    soap_message = get_soap_message(request)
    server = create_server(request)
    login = lasso.Login(server)
    try:
        login.processRequestMsg(force_str(soap_message))
    except (lasso.ProfileUnknownProviderError, lasso.ParamError):
        if not load_provider(request, login.remoteProviderId, server=login.server):
            logger.warning('provider loading failure')
        try:
            login.processRequestMsg(force_str(soap_message))
        except lasso.DsError as e:
            logger.warning('signature error for %s: %s', e, login.remoteProviderId)
        else:
            logger.debug('reloading artifact')
            reload_artifact(login)
    except Exception:
        logger.exception('resolve error')
    try:
        login.buildResponseMsg(None)
        logger.debug('resolve response %r', login.msgBody)
    except Exception:
        logger.exception('resolve error')
        return soap_fault(request, faultcode='soap:Server', faultstring='Internal Server Error')
    logger.debug('treatment ended, return answer')
    return return_saml_soap_response(login)


def check_delegated_authentication_permission(request):
    logger.debug('superuser? %s', request.user.is_superuser())
    return request.user.is_superuser()


@never_cache
@csrf_exempt
@login_required
def idp_sso(request, provider_id=None, return_profile=False):
    """Initiate an SSO toward provider_id without a prior AuthnRequest"""
    if not provider_id:
        provider_id = request.POST.get('provider_id')
    if not provider_id:
        return error_redirect(request, N_('missing provider identifier'))
    logger.debug('start of an idp initiated sso toward %s', provider_id)
    server = create_server(request)
    login = lasso.Login(server)
    liberty_provider = load_provider(request, provider_id, server=login.server)
    if not liberty_provider:
        return error_redirect(request, N_('provider %r is unknown'), provider_id)
    username = request.POST.get('username')
    if username:
        if not check_delegated_authentication_permission(request):
            return error_redirect(
                request,
                N_('%r tried to log as %r on %r but was forbidden'),
                request.user,
                username,
                provider_id,
            )
        try:
            user = User.objects.get_by_natural_key(username=username)
        except User.DoesNotExist:
            return error_redirect(request, N_('you cannot login as %r as it does not exist'), username)
    else:
        user = request.user
    policy = get_sp_options_policy(liberty_provider)
    # Control assertion consumer binding
    if not policy:
        return error_redirect(request, N_('missing service provider policy'))
    nid_format = policy.default_name_id_format
    if needs_persistence(nid_format):
        load_federation(request, get_entity_id(request), login, user)
    login.initIdpInitiatedAuthnRequest(provider_id)
    binding = policy.prefered_assertion_consumer_binding
    if binding == 'meta':
        pass
    elif binding == 'art':
        login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_ARTIFACT
    elif binding == 'post':
        login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST
    else:
        return error_redirect(request, N_('unknown binding %r') % binding)
    # Control nid format policy
    # XXX: if a federation exist, we should use transient
    login.request.nameIdPolicy.format = nidformat_to_saml2_urn(nid_format)
    login.request.nameIdPolicy.allowCreate = True

    login.processAuthnRequestMsg(None)
    logger.debug('nameId %r', nid_format)
    logger.debug('binding %r', binding)
    logger.debug('authentication request initialized toward provider_id %r', provider_id)

    return sso_after_process_request(
        request,
        login,
        consent_obtained=False,
        user=user,
        nid_format=nid_format,
        return_profile=return_profile,
    )


@never_cache
def finish_slo(request):
    id = request.GET.get('id')
    if not id:
        return HttpResponseBadRequest('finish_slo: missing id argument', content_type='text/plain')
    try:
        logout_dump, session_key = get_and_delete_key_values(id)
    except KeyError:
        messages.warning(request, _('request has expired'))
        return utils_misc.redirect(request, 'auth_homepage')
    server = create_server(request)
    logout = lasso.Logout.newFromDump(server, force_str(logout_dump))
    load_provider(request, logout.remoteProviderId, server=logout.server)
    # Clean all session
    all_sessions = LibertySession.objects.filter(django_session_key=session_key)
    try:
        logout.buildResponseMsg()
    except lasso.ProfileUnknownProfileUrlError:
        # SP sent us a logout request but we do not know its Response endpoint, just continue to the
        # homepage
        return redirect('auth_homepage')
    except lasso.Error as e:
        logger.warning('logout.buildResponseMsg() failed: %s', e)
        return redirect('auth_homepage')
    if all_sessions.exists():
        all_sessions.delete()
        set_saml2_response_responder_status_code(logout.response, lasso.SAML2_STATUS_CODE_PARTIAL_LOGOUT)
        logger.warning('partial logout')
        logout.buildResponseMsg()
    provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
    return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name)


def return_logout_error(request, logout, error):
    logout.buildResponseMsg()
    set_saml2_response_responder_status_code(logout.response, error)
    # Hack because response is not initialized before
    # buildResponseMsg
    logout.buildResponseMsg()
    logger.debug('returned an error message on logout: %s', error)
    provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
    return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name)


def process_logout_request(request, message, binding):
    '''Do the first part of processing a logout request'''
    server = create_server(request)
    logout = lasso.Logout(server)
    if not message:
        return logout, HttpResponseBadRequest('No message was present', content_type='text/plain')
    logger.debug('slo with binding %s message %s', binding, message)
    try:
        try:
            logout.processRequestMsg(force_str(message))
        except (lasso.ServerProviderNotFoundError, lasso.ProfileUnknownProviderError):
            logger.debug('loading provider %s', logout.remoteProviderId)
            p = load_provider(request, logout.remoteProviderId, server=logout.server)
            if not p:
                logger.warning('slo unknown provider %s', logout.remoteProviderId)
                return logout, return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER)
            policy = get_sp_options_policy(p)
            # we do not verify authn request, why verify logout requests...
            if not policy.authn_request_signed:
                logout.setSignatureVerifyHint(lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE)
            logout.processRequestMsg(force_str(message))
    except lasso.DsError:
        logger.warning('slo signature error')
        return logout, return_logout_error(request, logout, lasso.LIB_STATUS_CODE_INVALID_SIGNATURE)
    except Exception as e:
        logger.warning('slo unknown error when processing a request: %s', e)
        return logout, HttpResponseBadRequest('Invalid logout request', content_type='text/plain')
    if binding != 'SOAP' and not check_destination(request, logout.request):
        logger.warning('slo wrong or absent destination')
        return logout, return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_MISSING_DESTINATION)
    return logout, None


def log_logout_request(logout):
    name_id = nameid2kwargs(logout.request.nameId)
    session_indexes = logout.request.sessionIndexes
    logger.debug('slo nameid: %s session_indexes: %s', name_id, session_indexes)


def validate_logout_request(request, logout, idp=True):
    if not isinstance(logout.request.nameId, lasso.Saml2NameID):
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_MISSING_NAMEID)
    # only idp have the right to send logout request without session indexes
    if not logout.request.sessionIndexes and idp:
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX)


def logout_synchronous_other_backends(request, logout, django_sessions_keys):
    backends = get_idp_backends()
    if backends:
        logger.debug('backends %s', backends)
    else:
        logger.debug('no backends')
    ok = True
    for backend in backends:
        ok = ok and backend.can_synchronous_logout(django_sessions_keys)
    if not ok:
        return return_logout_error(request, logout, lasso.SAML2_STATUS_CODE_UNSUPPORTED_BINDING)
    logger.debug('treatments ended')
    return None


def get_only_last_session(issuer_id, provider_id, name_id, session_indexes):
    """Try to have a decent behaviour when receiving a logout request with
    multiple session indexes.

    Enumerate all emitted assertions for the given session, and for each
    provider only keep the more recent one.
    """
    lib_session1 = LibertySession.get_for_nameid_and_session_indexes(
        issuer_id, provider_id, name_id, session_indexes
    )
    django_session_keys = [s.django_session_key for s in lib_session1]
    lib_session = LibertySession.objects.filter(django_session_key__in=django_session_keys)
    providers = {s.provider_id for s in lib_session}
    result = []
    for provider in providers:
        if provider != provider_id:
            x = lib_session.filter(provider_id=provider)
            latest = x.latest('creation')
            result.append(latest)
    if lib_session1:
        logger.debug('last session %s' % lib_session1)
    return lib_session1, result, django_session_keys


def build_session_dump(liberty_sessions):
    """Build a session dump from a list of pairs
    (provider_id,assertion_content)"""
    session = [
        '<Session xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"'
        ' xmlns="http://www.entrouvert.org/namespaces/lasso/0.0" Version="2">',
    ]
    for liberty_session in liberty_sessions:
        session.append(
            '<NidAndSessionIndex ProviderID="{0.provider_id}" '
            'AssertionID="xxx" '
            'SessionIndex="{0.session_index}">'.format(liberty_session)
        )
        session.append(f'<saml:NameID Format="{liberty_session.name_id_format}" ')
        if liberty_session.name_id_qualifier:
            session.append(f'NameQualifier="{liberty_session.name_id_qualifier}" ')
        if liberty_session.name_id_sp_name_qualifier:
            session.append(f'SPNameQualifier="{liberty_session.name_id_sp_name_qualifier}" ')
        session.append(f'>{liberty_session.name_id_content}</saml:NameID>')
        session.append('</NidAndSessionIndex>')
    session.append('</Session>')
    s = ''.join(session)
    logger.debug('session built %s', s)
    return s


def set_session_dump_from_liberty_sessions(profile, lib_sessions):
    """Extract all assertion from a list of lib_sessions, and create a session
    dump from them"""
    session_dump = build_session_dump(lib_sessions)
    profile.setSessionFromDump(session_dump)


@require_POST
@never_cache
@csrf_exempt
def slo_soap(request):
    """Endpoint for receiveing saml2:AuthnRequest by SOAP"""
    message = get_soap_message(request)
    if not message:
        return HttpResponseBadRequest('Bad SOAP message', content_type='text/plain')
    logger.debug('soap message received %s', message)
    logout, error = process_logout_request(request, message, 'SOAP')
    if error:
        return error
    error = validate_logout_request(request, logout, idp=True)
    if error:
        return error

    try:
        provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
    except ObjectDoesNotExist:
        logger.warning('provider %r unknown', logout.remoteProviderId)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
    policy = get_sp_options_policy(provider)
    if not policy:
        logger.warning('No policy found for %s', logout.remoteProviderId)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
    if not policy.accept_slo:
        logger.warning('received slo from %s not authorized', logout.remoteProviderId)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED)

    '''Find all active sessions on SPs but the SP initiating the SLO'''
    found, lib_sessions, django_session_keys = get_only_last_session(
        logout.server.providerId,
        logout.remoteProviderId,
        logout.request.nameId,
        logout.request.sessionIndexes,
    )
    if not found:
        logger.debug('no third SP session found')
    else:
        logger.debug('begin SP sessions processing...')
        for lib_session in lib_sessions:
            p = load_provider(request, lib_session.provider_id, server=logout.server)
            if not p:
                logger.debug('slo cannot logout provider %s, it is no more known.', lib_session.provider_id)
                continue
            else:
                logger.debug('provider %s loaded', p)
                policy = get_sp_options_policy(p)
                if not policy:
                    logger.warning('No policy found for %s', lib_session.provider_id)
                elif not policy.forward_slo:
                    logger.debug('%s configured not to receive slo', lib_session.provider_id)
                if not policy or not policy.forward_slo:
                    lib_sessions.remove(lib_session)
        set_session_dump_from_liberty_sessions(logout, found[0:1] + lib_sessions)
        try:
            logout.validateRequest()
        except lasso.LogoutUnsupportedProfileError:
            """
            If one provider does not support SLO by SOAP,
            continue with others!
            """
            logger.warning(
                'one provider does not support SOAP among %s', [s.provider_id for s in lib_sessions]
            )
        except Exception as e:
            logger.warning('slo, unknown error %s', e)
            logout.buildResponseMsg()
            provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
            return return_saml2_response(
                request, logout, title=_('You are being redirected to "%s"') % provider.name
            )
        for lib_session in lib_sessions:
            try:
                logger.debug('slo, relaying logout to provider %s', lib_session.provider_id)
                '''
                    As we are in a synchronous binding, we need SOAP support
                '''
                logout.initRequest(lib_session.provider_id, lasso.HTTP_METHOD_SOAP)
                logout.buildRequestMsg()
                if logout.msgBody:
                    logger.debug('slo by SOAP')
                    soap_response = send_soap_request(request, logout)
                    logout.processResponseMsg(soap_response)
                else:
                    logger.debug('Provider does not support SOAP')
            except lasso.Error as e:
                logger.warning('slo, relaying to %s failed: %s', lib_session.provider_id, e)

    '''
        Respond to the SP initiating SLO
    '''
    try:
        logout.buildResponseMsg()
    except lasso.Error as e:
        logger.warning('slo failure to build reponse msg: %s', e)
        raise NotImplementedError()
    logger.debug('processing finished')
    logger.debug('kill django sessions')
    kill_django_sessions(django_session_keys)
    provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
    return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name)


@never_cache
@csrf_exempt
def slo(request):
    """Endpoint for receiving SLO by POST, Redirect."""
    message = get_saml2_request_message_async_binding(request)
    logout, response = process_logout_request(request, message, request.method)
    if response:
        return response

    try:
        provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
    except ObjectDoesNotExist:
        logger.debug('provider %r unknown', logout.remoteProviderId)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
    policy = get_sp_options_policy(provider)
    if not policy:
        logger.debug('No policy found for %s', logout.remoteProviderId)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
    if not policy.accept_slo:
        logger.debug('received slo from %s not authorized', logout.remoteProviderId)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNAUTHORIZED)

    try:
        try:
            logout.processRequestMsg(force_str(message))
        except (lasso.ServerProviderNotFoundError, lasso.ProfileUnknownProviderError):
            load_provider(request, logout.remoteProviderId, server=logout.server)
            logout.processRequestMsg(force_str(message))
    except lasso.DsError as e:
        logger.warning('signature error %s', e)
        logout.buildResponseMsg()
        provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
        return return_saml2_response(
            request, logout, title=_('You are being redirected to "%s"') % provider.name
        )
    except (lasso.ProfileInvalidMsgError, lasso.ProfileMissingIssuerError):
        return error_page(request, _('Invalid logout request'), logger=logger, warning=True)
    session_indexes = logout.request.sessionIndexes
    if len(session_indexes) == 0:
        logger.warning(
            'slo received a request from %s without any SessionIndex, it is forbidden',
            logout.remoteProviderId,
        )
        logout.buildResponseMsg()
        provider = LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
        return return_saml2_response(
            request, logout, title=_('You are being redirected to "%s"') % provider.name
        )
    logger.debug('asynchronous slo from %s', logout.remoteProviderId)
    # Filter sessions
    if not logout.request.nameId:
        logger.warning('slo refused, no NameID in the SLO request')
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_MISSING_NAMEID)
    all_sessions = LibertySession.get_for_nameid_and_session_indexes(
        logout.server.providerId,
        logout.remoteProviderId,
        logout.request.nameId,
        logout.request.sessionIndexes,
    )
    if not all_sessions.exists():
        logger.warning('slo refused, since no session exists with the requesting provider')
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION)
    # Load session dump for the requesting provider
    last_session = all_sessions.latest('creation')
    set_session_dump_from_liberty_sessions(logout, [last_session])
    try:
        logout.validateRequest()
    except lasso.Error as e:
        logger.warning('logout request validation failed: %s', e)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR)
    except Exception as e:
        logger.warning('internal error: %s', e)
        return return_logout_error(request, logout, AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR)
    # Now clean sessions for this provider
    LibertySession.objects.filter(
        provider_id=logout.remoteProviderId, django_session_key=request.session.session_key
    ).delete()
    # Save some values for cleaning up
    save_key_values(logout.request.id, force_text(logout.dump()), request.session.session_key)

    # Use the logout view and come back to the finish slo view
    next_url = make_url(finish_slo, params={'id': logout.request.id})
    return a2_views.logout(request, next_url=next_url, do_local=False, check_referer=False)


def icon_url(name):
    return '%s/authentic2/images/%s.png' % (settings.STATIC_URL, name)


def ko_icon(request):
    return HttpResponseRedirect(icon_url('ko'), status=307)


def ok_icon(request):
    return HttpResponseRedirect(icon_url('ok'), status=307)


@never_cache
@csrf_exempt
@login_required
def idp_slo(request, provider_id=None):
    """Send a single logout request to a SP, if given a next parameter, return
    to this URL, otherwise redirect to an icon symbolizing failure or success
    of the request

    provider_id - entity id of the service provider to log out
    all - if present, logout all sessions by omitting the SessionIndex element
    """
    all = request.POST.get('all')
    next = request.POST.get('next')

    if request.method == 'GET' and 'provider_id' in request.GET:
        provider_id = request.GET.get('provider_id')
    if request.method == 'POST' and 'provider_id' in request.POST:
        provider_id = request.POST.get('provider_id')
    if not provider_id:
        logger.debug('to initiate a slo we need a provider_id')
        return redirect_next(request, next) or ko_icon(request)
    logger.debug('provider_id is %s', provider_id)

    server = create_server(request)
    logout = lasso.Logout(server)

    provider = load_provider(request, provider_id, server=logout.server)
    if not provider:
        logger.warning('slo failed to load provider')
        return redirect_next(request, next) or ko_icon(request)
    policy = get_sp_options_policy(provider)
    if not policy:
        logger.warning('No policy found for %s', provider_id)
        return redirect_next(request, next) or ko_icon(request)
    if not policy.forward_slo:
        logger.warning('slo asked for %s configured to not reveive slo', provider_id)
        return redirect_next(request, next) or ko_icon(request)

    lib_sessions = LibertySession.objects.filter(
        django_session_key=request.session.session_key, provider_id=provider_id
    )
    if lib_sessions:
        logger.debug('%d lib_sessions found', lib_sessions.count())
        set_session_dump_from_liberty_sessions(logout, [lib_sessions[0]])
    try:
        logout.initRequest(provider_id, policy.http_method_for_slo_request)
    except (lasso.ProfileMissingAssertionError, lasso.ProfileSessionNotFoundError):
        logger.debug('slo failed because no sessions exists for %r', provider_id)
        return redirect_next(request, next) or ko_icon(request)
    if all is not None:
        logout.request.sessionIndexes = []
    else:
        session_indexes = lib_sessions.values_list('session_index', flat=True)
        logout.request.sessionIndexes = tuple(force_str(session_index) for session_index in session_indexes)
    logout.msgRelayState = logout.request.id
    try:
        logout.buildRequestMsg()
    except Exception as e:
        logger.warning('slo misc error: %s', e)
        return redirect_next(request, next) or ko_icon(request)
    if logout.msgBody:
        try:
            soap_response = send_soap_request(request, logout)
        except Exception as e:
            logger.warning('slo SOAP failure due to %s', e)
            return redirect_next(request, next) or ko_icon(request)
        return process_logout_response(request, logout, soap_response, next)
    else:
        save_key_values(logout.request.id, force_text(logout.dump()), provider_id, next)
        return HttpResponseRedirect(logout.msgUrl)


def process_logout_response(request, logout, soap_response, next):
    logger.debug('logout response is %r', soap_response)
    try:
        logout.processResponseMsg(force_str(soap_response))
    except getattr(lasso, 'ProfileRequestDeniedError', lasso.LogoutRequestDeniedError):
        logger.warning('%s denied the logout request', logout.remoteProviderId)
        return redirect_next(request, next) or ko_icon(request)
    except Exception as e:
        logger.warning('slo error: %s', e)
    else:
        LibertySession.objects.filter(
            django_session_key=request.session.session_key, provider_id=logout.remoteProviderId
        ).delete()
        logger.debug('deleted session to %s', logout.remoteProviderId)
    return redirect_next(request, next) or ok_icon(request)


@never_cache
def slo_return(request):
    relay_state = request.GET.get('RelayState')
    if not relay_state:
        return error_redirect(request, N_('slo no relay state in response'), default_url=icon_url('ko'))
    logger.debug('relay_state %r', relay_state)
    try:
        logout_dump, provider_id, next = get_and_delete_key_values(relay_state)
    except KeyError:
        return error_redirect(request, N_('unknown relay state %r'), relay_state, default_url=icon_url('ko'))
    server = create_server(request)
    logout = lasso.Logout.newFromDump(server, force_str(logout_dump))
    provider_id = logout.remoteProviderId
    # forced to reset signature_verify_hint as it is not saved in the dump
    provider = load_provider(request, provider_id, server=server)
    policy = get_sp_options_policy(provider)
    # FIXME: should use a logout_request_signature_check_hint
    if not policy.authn_request_signed:
        logout.setSignatureVerifyHint(lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE)
    if not load_provider(request, provider_id, server=logout.server):
        logger.warning('failed to load provider %s', provider_id)
    return process_logout_response(request, logout, get_saml2_query_request(request), next)


# Helpers

# Mapping to generate the metadata file, must be kept in sync with the url
# dispatcher


def get_options():
    options = {
        'key': app_settings.SIGNATURE_PUBLIC_KEY,
        'private_key': app_settings.SIGNATURE_PRIVATE_KEY,
    }
    options.update(app_settings.METADATA_OPTIONS)
    return options


def create_server(request, provider_id=None):
    """Build a lasso.Server object using current settings for the IdP

    The built lasso.Server is cached for later use it should work until
    multithreading is used, then thread local storage should be used.
    """
    options = get_options()
    __cached_server = create_saml2_server(request, idp_map=metadata_map, options=options)
    return __cached_server


def log_info_authn_request_details(login):
    '''Push to logs details abour the received AuthnRequest'''
    request = login.request
    details = {
        'issuer': login.request.issuer and login.request.issuer.content,
        'forceAuthn': login.request.forceAuthn,
        'isPassive': login.request.isPassive,
        'protocolBinding': login.request.protocolBinding,
    }
    nameIdPolicy = request.nameIdPolicy
    if nameIdPolicy:
        details['nameIdPolicy'] = {
            'allowCreate': nameIdPolicy.allowCreate,
            'format': nameIdPolicy.format,
            'spNameQualifier': nameIdPolicy.spNameQualifier,
        }
    logger.debug('%r' % details)


def check_destination(request, req_or_res):
    '''Check that a SAML message Destination has the proper value'''
    destination = request.build_absolute_uri(request.path)
    result = req_or_res.destination == destination
    if not result:
        logger.warning('failure, expected: %r got: %r ', destination, req_or_res.destination)
    return result


def error_redirect(request, msg, *args, **kwargs):
    """Log a warning message, register it with the messages framework, then
    redirect the user to the homepage.

    It will redirect to Authentic2 homepage unless a next query parameter was used.
    """
    default_kwargs = {
        'log_level': logging.WARNING,
        'msg_level': messages.WARNING,
        'default_url': None,
    }
    default_kwargs.update(kwargs)
    messages.add_message(request, default_kwargs['msg_level'], _(msg) % args)
    logger.log(default_kwargs['log_level'], msg, *args)
    next_url = request.GET.get(REDIRECT_FIELD_NAME)
    if next_url:
        return HttpResponseRedirect(next_url)
    default_url = kwargs.get('default_url')
    if default_url:
        return HttpResponseRedirect(default_url)
    else:
        return redirect('auth_homepage')
