1649 lines
68 KiB
Python
1649 lines
68 KiB
Python
# 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')
|