authentic/authentic2/idp/saml/saml2_endpoints.py

1599 lines
67 KiB
Python

"""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 logging
import urllib
import xml.etree.cElementTree as ctree
import hashlib
import random
import string
import lasso
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, HttpResponseRedirect, \
HttpResponseForbidden, HttpResponseBadRequest, Http404
from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import BACKEND_SESSION_KEY
from django.conf import settings
from django.utils.encoding import smart_unicode
from django.contrib.auth import load_backend
from authentic2.compat import get_user_model
import authentic2.idp as idp
import authentic2.idp.views as idp_views
from authentic2.idp.models import get_attribute_policy
from authentic2.saml.models import LibertyAssertion, LibertyArtifact, \
LibertySession, LibertyFederation, LibertySessionDump, \
nameid2kwargs, saml2_urn_to_nidformat, LIBERTY_SESSION_DUMP_KIND_SP, \
nidformat_to_saml2_urn, save_key_values, get_and_delete_key_values, \
LibertyProvider, LibertyServiceProvider, NAME_ID_FORMATS
from authentic2.saml.common import redirect_next, asynchronous_bindings, \
soap_bindings, load_provider, get_saml2_request_message, \
error_page, set_saml2_response_responder_status_code, \
AUTHENTIC_STATUS_CODE_MISSING_DESTINATION, \
load_federation, load_session, \
return_saml2_response, save_session, \
get_soap_message, soap_fault, return_saml_soap_response, \
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER, \
AUTHENTIC_STATUS_CODE_MISSING_NAMEID, \
AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX, \
AUTHENTIC_STATUS_CODE_UNKNOWN_SESSION, \
AUTHENTIC_STATUS_CODE_INTERNAL_SERVER_ERROR, \
AUTHENTIC_STATUS_CODE_UNAUTHORIZED, \
send_soap_request, get_saml2_query_request, \
get_saml2_request_message_async_binding, create_saml2_server, \
get_saml2_metadata, get_sp_options_policy, get_idp_options_policy, \
get_entity_id
import authentic2.saml.saml2utils as saml2utils
from authentic2.models import AuthenticationEvent
from common import redirect_to_login, kill_django_sessions
from authentic2.constants import NONCE_FIELD_NAME
from authentic2.idp import signals as idp_signals
# from authentic2.idp.models import *
from authentic2.utils import (cache_and_validate, get_backends as
get_idp_backends, get_username)
from authentic2.decorators import is_transient_user
logger = logging.getLogger('authentic2.idp.saml')
def get_nonce():
alphabet = string.letters+string.digits
return '_'+''.join(random.SystemRandom().choice(alphabet) for i in xrange(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')
)
metadata_options = {'key': settings.SAML_SIGNATURE_PUBLIC_KEY}
@cache_and_validate(settings.LOCAL_METADATA_CACHE_TIMEOUT)
def metadata(request):
'''Endpoint to retrieve the metadata file'''
logger.info('return metadata')
return HttpResponse(get_metadata(request, request.path),
mimetype='text/xml')
#####
# 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, openid without accounts)
# 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 %r', assertion.id)
# Use assertion ID as session index
assertion.authnStatement[0].sessionIndex = assertion.id
logger.debug("nid_format is %r", nid_format)
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(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':
username = get_username(request.user)
assert username, 'username is required when using the username NameID format'
assertion.subject.nameID.content = username
if nid_format == 'edupersontargetedid':
assertion.subject.nameID.format = NAME_ID_FORMATS[nid_format]['samlv2']
keys = ''.join([get_username(request.user),
provider_id, settings.SECRET_KEY])
edu_person_targeted_id = '_' + hashlib.sha1(keys).hexdigest().upper()
assertion.subject.nameID.content = edu_person_targeted_id
attribute_definition = ('urn:oid:1.3.6.1.4.1.5923.1.1.1.10',
lasso.SAML2_ATTRIBUTE_NAME_FORMAT_URI, 'eduPersonTargetedID')
value = assertion.subject.nameID.exportToXml()
value = ctree.fromstring(value)
saml2_add_attribute_values(assertion,
{ attribute_definition: [ value ]})
logger.info('adding an eduPersonTargetedID attribute with value %s',
edu_person_targeted_id)
assertion.subject.nameID.format = NAME_ID_FORMATS[nid_format]['samlv2']
def saml2_add_attribute_values(assertion, attributes):
if not attributes:
logger.info("\
there are no attributes to add")
else:
logger.info("there are attributes to add")
logger.debug("\
assertion before processing %s" % assertion.dump())
logger.debug("adding attributes %s" \
% str(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:
try:
# duck type the ElemenTree interface
value.makeelement and value.tag
text_node = lasso.MiscTextNode.\
newWithXmlNode(ctree.tostring(value))
except AttributeError:
if value is True:
value = u'true'
elif value is False:
value = u'false'
else:
value = smart_unicode(value)
value = value.encode('utf-8')
text_node = lasso.MiscTextNode.newWithString(value)
text_node.textChild = True
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" % assertion.dump())
def build_assertion(request, login, nid_format='transient', attributes=None):
"""After a successfully validated authentication request, build an
authentication assertion
"""
now = datetime.datetime.utcnow()
logger.info("building assertion at %s" % str(now))
logger.debug('named Id format is %s' % nid_format)
# 1 minute ago
notBefore = now - datetime.timedelta(0, __delta)
# 1 minute in the future
notOnOrAfter = now + datetime.timedelta(0, __delta)
ssl = 'HTTPS' in request.environ
if __user_backend_from_session:
backend = request.session[BACKEND_SESSION_KEY]
logger.debug("authentication from session %s" \
% backend)
if backend in ('django.contrib.auth.backends.ModelBackend',
'authentic2.idp.auth_backends.LogginBackend',
'django_auth_ldap.backend.LDAPBackend'):
authn_context = lasso.SAML2_AUTHN_CONTEXT_PASSWORD
elif backend == 'authentic2.auth2_auth.auth2_ssl.backend.SSLBackend':
authn_context = lasso.SAML2_AUTHN_CONTEXT_X509
else:
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:
auth_event = AuthenticationEvent.objects.\
get(nonce=login.request.id)
logger.debug("authentication from stored event "
"%s" % auth_event)
if auth_event.how == 'password':
authn_context = lasso.SAML2_AUTHN_CONTEXT_PASSWORD
elif auth_event.how == 'password-on-https':
authn_context = \
lasso.SAML2_AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT
elif auth_event.how == 'ssl':
authn_context = lasso.SAML2_AUTHN_CONTEXT_X509
elif auth_event.how.startswith('oath-totp'):
authn_context = lasso.SAML2_AUTHN_CONTEXT_TIME_SYNC_TOKEN
else:
raise NotImplementedError('Unknown authentication method %r' \
% auth_event.how)
except ObjectDoesNotExist:
# TODO: previous session over secure transport (ssl) ?
authn_context = lasso.SAML2_AUTHN_CONTEXT_PREVIOUS_SESSION
logger.info("authn_context %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
logger.debug("assertion building in progress %s" \
% assertion.dump())
logger.debug("fill assertion")
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)
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.info("nameID persistent, new federation")
federation.save()
else:
logger.info("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.info("sending nameID %(name_id_format)s: "
"%(name_id_content)s to %(entity_id)s for user %(user)s" % kwargs)
if attributes:
logger.debug("add attributes to the assertion")
saml2_add_attribute_values(login.assertion, attributes)
register_new_saml2_session(request, login)
@csrf_exempt
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.
"""
logger.info("performing sso")
if request.method == "GET":
logger.debug('called by GET')
consent_answer = request.GET.get('consent_answer', '')
if consent_answer:
logger.info('back from the consent page for federation with \
answer %s' % consent_answer)
message = get_saml2_request_message(request)
server = create_server(request)
login = lasso.Login(server)
# 1. Process the request, separate POST and GET treatment
if not message:
logger.warn("missing query string")
return HttpResponseForbidden("A SAMLv2 Single Sign On request need a "
"query string")
logger.debug('processing sso request %r' % message)
policy = None
signed = True
while True:
try:
login.processAuthnRequestMsg(message)
break
except (lasso.ProfileInvalidMsgError,
lasso.ProfileMissingIssuerError,), e:
logger.error('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: %r") % message)
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.error("the request cannot be answered because no "
"valid protocol binding could be found")
return HttpResponseBadRequest(message)
except lasso.DsError, e:
log_info_authn_request_details(login)
logger.error('digital signature treatment error: %s' % e)
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:
message = _('sso: fail to load unknown provider %s' \
% provider_id)
return error_page(request, message, logger=logger,
warning=True)
else:
policy = get_sp_options_policy(provider_loaded)
if not policy:
logger.error('No policy defined')
return error_page(request, _('sso: No SP policy defined'),
logger=logger, warning=True)
logger.info('provider %s loaded with success' \
% provider_id)
if provider_loaded.service_provider.policy.authn_request_signature_check_hint == lasso.PROFILE_SIGNATURE_VERIFY_HINT_IGNORE:
signed = False
login.setSignatureVerifyHint(
provider_loaded.service_provider.policy \
.authn_request_signature_check_hint)
if signed and not check_destination(request, login.request):
logger.error('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
logger.debug('nameID policy is %s' % name_id_policy.dump())
if name_id_policy.format and \
name_id_policy.format != \
lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
nid_format = saml2_urn_to_nidformat(name_id_policy.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' \
% str(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.error('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'
logger.debug('set nameID policy format %s' % nid_format)
name_id_policy.format = nidformat_to_saml2_urn(nid_format)
return sso_after_process_request(request, login, nid_format=nid_format)
def need_login(request, login, save, nid_format):
"""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, login.dump(), False, save, nid_format)
url = reverse(continue_sso) + '?%s=%s' % (NONCE_FIELD_NAME, nonce)
logger.debug('redirect to login page with next url %s' % url)
return redirect_to_login(url,
other_keys={NONCE_FIELD_NAME: nonce})
def get_url_with_nonce(request, function, nonce):
url = reverse(function) + '?%s=%s' % (NONCE_FIELD_NAME, nonce)
return urllib.quote(url)
def need_consent_for_federation(request, login, save, nid_format):
nonce = login.request.id or get_nonce()
save_key_values(nonce, login.dump(), False, save, 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 = urllib.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)
def need_consent_for_attributes(request, login, consent_obtained, save,
nid_format):
nonce = login.request.id or get_nonce()
save_key_values(nonce, login.dump(), consent_obtained, save, 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 = urllib.quote(login.request.issuer.content)
url = '%s?%s=%s&next=%s&provider_id=%s' \
% (reverse('a2-consent-attributes'), NONCE_FIELD_NAME,
nonce, get_url_with_nonce(request, continue_sso, nonce),
display_name)
logger.debug('redirect to url %s' % url)
return HttpResponseRedirect(url)
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.info("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.info("back from the consent page for "
"attributes %s" % consent_attribute_answer)
nonce = request.REQUEST.get(NONCE_FIELD_NAME, '')
if not nonce:
logger.warning('nonce not found')
return HttpResponseBadRequest()
login_dump, consent_obtained, save, nid_format = \
get_and_delete_key_values(nonce)
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, 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.info('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.info("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.info("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 sso_after_process_request(request, login, consent_obtained=False,
consent_attribute_answer=False, user=None, save=True,
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.
save: whether to save the result of this transaction or not.
"""
nonce = login.request.id
user = user or request.user
did_auth = AuthenticationEvent.objects.filter(nonce=nonce).exists()
force_authn = login.request.forceAuthn
passive = login.request.isPassive
logger.debug('named Id format is %s' \
% nid_format)
if not passive and \
(user.is_anonymous() or (force_authn and not did_auth)):
logger.info('login required')
return need_login(request, login, save, nid_format)
#Deal with transient users
transient_user = False
# XXX: Deal with all kind of transient users
if is_transient_user(request.user):
logger.debug('the user is transient')
transient_user = True
if transient_user and login.request.nameIdPolicy.format == \
lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
logger.info("access denied, the user is "
"transient and the sp ask for persistent")
set_saml2_response_responder_status_code(login.response,
lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
return finish_sso(request, login)
# If the sp does not allow create, reject
if transient_user and login.request.nameIdPolicy.allowCreate == 'false':
logger.info("access denied, we created a "
"transient user and allow creation is not authorized by the SP")
set_saml2_response_responder_status_code(login.response,
lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
return finish_sso(request, login)
#Do not ask consent for federation if a transient nameID is provided
transient = False
if login.request.nameIdPolicy.format == \
lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
transient = True
attributes = {}
if 'attributes_loaded' in request.session:
logger.debug('attributes already loaded')
attributes = request.session['attributes_loaded']
else:
logger.debug('attributes loading...')
attributes_provided = \
idp_signals.add_attributes_to_response.send(sender=None,
request=request, user=request.user,
audience=login.remoteProviderId)
logger.info(''
'signal add_attributes_to_response sent')
for attrs in attributes_provided:
logger.info('add_attributes_to_response '
'connected to function %s' % attrs[0].__name__)
if attrs[1] and 'attributes' in attrs[1]:
dic = attrs[1]
logger.info('attributes provided are '
'%s' % str(dic['attributes']))
for key in dic['attributes'].keys():
attributes[key] = dic['attributes'][key]
request.session['attributes_loaded'] = attributes
decisions = idp_signals.authorize_service.send(sender=None,
request=request, user=request.user, audience=login.remoteProviderId,
attributes=attributes)
logger.info('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.info('authorize_service connected '
'to function %s' % decision[0].__name__)
dic = decision[1]
if dic and 'authz' in dic:
logger.info('decision is %s' \
% dic['authz'])
if 'message' in dic:
logger.info('with message %s' \
% dic['message'])
if not dic['authz']:
logger.info('access denied by '
'an external function')
access_granted = False
else:
logger.info('no function connected to '
'authorize_service')
if not access_granted:
logger.info('access denied, return answer '
'to the requester')
set_saml2_response_responder_status_code(login.response,
lasso.SAML2_STATUS_CODE_REQUEST_DENIED)
return finish_sso(request, login)
provider = load_provider(request, login.remoteProviderId,
server=login.server)
if not provider:
logger.info(''
'sso for an unknown provider %s' % login.remoteProviderId)
return error_page(request,
_('Provider %s is unknown') % login.remoteProviderId,
logger=logger)
saml_policy = get_sp_options_policy(provider)
if not saml_policy:
logger.error('No policy defined for '
'provider %s' % login.remoteProviderId)
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' \
% str(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' \
% str(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.info('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, save, nid_format)
attribute_policy = get_attribute_policy(provider)
if not attribute_policy and attributes:
logger.info('no attribute policy, we do '
'not forward attributes')
attributes = None
elif attribute_policy and attribute_policy.ask_consent_attributes and attributes:
if not consent_attribute_answer:
logger.info('consent for attribute '
'propagation')
request.session['attributes_to_send'] = attributes
request.session['allow_attributes_selection'] = \
attribute_policy.allow_attributes_selection
return need_consent_for_attributes(request, login,
consent_obtained, save, nid_format)
if consent_attribute_answer == 'accepted' and \
attribute_policy.allow_attributes_selection:
attributes = request.session['attributes_to_send']
elif consent_attribute_answer == 'refused':
attributes = None
logger.debug(''
'login dump before processing %s' % login.dump())
try:
if needs_persistence(nid_format):
logger.debug('load identity dump')
load_federation(request, get_entity_id(request, reverse(metadata)), login, user)
load_session(request, login)
logger.debug('load session')
login.validateRequestMsg(not user.is_anonymous(), consent_obtained)
logger.debug('validateRequestMsg %s' \
% login.dump())
except lasso.LoginRequestDeniedError:
logger.error('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, save=save)
except lasso.LoginFederationNotFoundError:
logger.error('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, save=save)
login.response.consent = consent_value
build_assertion(request, login, nid_format=nid_format,
attributes=attributes)
return finish_sso(request, login, user=user, save=save, 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('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.info('sending Artifact to assertionConsumer %r' % login.msgUrl)
save_artifact(request, login)
elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
login.buildAuthnResponseMsg()
logger.info('sending POST to assertionConsumer %r' % login.msgUrl)
logger.debug('POST content %r' % login.msgBody)
else:
logger.error('NotImplementedError with login %s' % login.dump())
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, save=False, return_profile=False):
logger.info('finishing sso...')
if user is None:
logger.debug('user is None')
user = request.user
response = return_login_response(request, login)
if save:
save_session(request, login)
logger.debug('session saved')
logger.info('sso treatment ended, send response')
if return_profile:
return login
return response
def save_artifact(request, login):
'''Remember an artifact message for later retrieving'''
LibertyArtifact(artifact=login.artifact,
content=login.artifactMessage.decode('utf-8'),
provider_id=login.remoteProviderId).save()
logger.debug('artifact saved')
def reload_artifact(login):
try:
art = LibertyArtifact.objects.get(artifact=login.artifact)
logger.debug('artifact found')
login.artifactMessage = art.content.encode('utf-8')
logger.debug('artifact loaded')
art.delete()
logger.debug('artifact deleted')
except ObjectDoesNotExist:
logger.debug('no artifact found')
pass
@csrf_exempt
def artifact(request):
'''Resolve a SAMLv2 ArtifactResolve request
'''
logger.info('soap call received')
soap_message = get_soap_message(request)
logger.debug('soap message %r' % soap_message)
server = create_server(request)
login = lasso.Login(server)
try:
login.processRequestMsg(soap_message)
except (lasso.ProfileUnknownProviderError, lasso.ParamError):
if not load_provider(request, login.remoteProviderId,
server=login.server):
logger.error('provider loading failure')
try:
login.processRequestMsg(soap_message)
except lasso.DsError, e:
logger.error('signature error for %s: %s'
% (e, login.remoteProviderId))
else:
logger.info('reloading artifact')
reload_artifact(login)
except:
logger.exception('resolve error')
try:
login.buildResponseMsg(None)
logger.debug('resolve response %s' % login.msgBody)
except:
logger.exception('resolve error')
return soap_fault(faultcode='soap:Server',
faultstring='Internal Server Error')
logger.info('treatment ended, return answer')
return return_saml_soap_response(login)
def check_delegated_authentication_permission(request):
logger.info('superuser? %s' \
% str(request.user.is_superuser()))
return request.user.is_superuser()
@csrf_exempt
@login_required
def idp_sso(request, provider_id=None, user_id=None, nid_format=None,
save=True, return_profile=False):
'''Initiate an SSO toward provider_id without a prior AuthnRequest
'''
User = get_user_model()
if request.method == 'GET':
logger.info('to initiate a sso we need a post form')
return error_page(request,
_('Error trying to initiate a single sign on'), logger=logger)
if not provider_id:
provider_id = request.POST.get('provider_id')
if not provider_id:
logger.info('to initiate a sso we need a provider_id')
return error_page(request,
_('A provider identifier was not provided'), logger=logger)
logger.info('sso initiated with %(provider_id)s' \
% {'provider_id': provider_id})
if user_id:
logger.info('sso as %s' % user_id)
server = create_server(request)
login = lasso.Login(server)
liberty_provider = load_provider(request, provider_id,
server=login.server)
if not liberty_provider:
logger.info('sso for an unknown provider %s' % provider_id)
return error_page(request, _('Provider %s is unknown') % provider_id,
logger=logger)
if user_id:
user = User.get(id=user_id)
if not check_delegated_authentication_permission(request):
logger.warning('%r tried to log as %r on %r but was '
'forbidden' % (request.user, user, provider_id))
return HttpResponseForbidden('You must be superuser to log as '
'another user')
else:
user = request.user
logger.info('sso by %r' % user)
policy = get_sp_options_policy(liberty_provider)
if nid_format:
logger.debug('nameId format is %r' % nid_format)
if not nid_format in policy.accepted_name_id_format:
logger.error('name id format %r is not supported by %r' \
% (nid_format, provider_id))
raise Http404('Provider %r does not support this name id format' \
% provider_id)
if not nid_format:
nid_format = policy.default_name_id_format
logger.debug('nameId format is %r' % nid_format)
if needs_persistence(nid_format):
load_federation(request, get_entity_id(request, reverse(metadata)), login, user)
logger.debug('federation loaded')
login.initIdpInitiatedAuthnRequest(provider_id)
# Control assertion consumer binding
if not policy:
logger.error('No policy defined, \
unable to set protocol binding')
return error_page(request, _('idp_sso: No SP policy defined'),
logger=logger)
binding = policy.prefered_assertion_consumer_binding
logger.debug('binding is %r' % 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:
logger.error('unsupported protocol binding %r' % binding)
return error_page(request, _('Server error'), logger=logger)
# 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)
return sso_after_process_request(request, login,
consent_obtained=False, user=user, save=save,
nid_format=nid_format, return_profile=return_profile)
def finish_slo(request):
id = request.REQUEST.get('id')
if not id:
logger.error('missing id argument')
return HttpResponseBadRequest('finish_slo: missing id argument')
logout_dump, session_key = get_and_delete_key_values(id)
server = create_server(request)
logout = lasso.Logout.newFromDump(server, logout_dump)
load_provider(request, logout.remoteProviderId, server=logout.server)
# Clean all session
all_sessions = \
LibertySession.objects.filter(django_session_key=session_key)
if all_sessions.exists():
all_sessions.delete()
return return_logout_error(request, logout,
lasso.SAML2_STATUS_CODE_PARTIAL_LOGOUT)
try:
logout.buildResponseMsg()
except:
logger.exception('failure to build reponse msg')
pass
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('send an error message %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')
logger.debug('slo with binding %s message %s' \
% (binding, message))
try:
try:
logout.processRequestMsg(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.error(''
'slo unknown provider %s' % logout.remoteProviderId)
return logout, return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_UNKNOWN_PROVIDER)
# we do not verify authn request, why verify logout requests...
logout.setSignatureVerifyHint(
p.service_provider.policy \
.authn_request_signature_check_hint)
logout.processRequestMsg(message)
except lasso.DsError:
logger.error(''
'slo signature error on request %s' % message)
return logout, return_logout_error(request, logout,
lasso.LIB_STATUS_CODE_INVALID_SIGNATURE)
except Exception:
logger.exception(''
'slo unknown error when processing a request %s' % message)
return logout, HttpResponseBadRequest('Invalid logout request')
if binding != 'SOAP' and not check_destination(request, logout.request):
logger.error(''
'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.info('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):
logger.error('slo request lacks a NameID')
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:
logger.error(''
'slo request lacks SessionIndex')
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_MISSING_SESSION_INDEX)
logger.info('valid logout request')
log_logout_request(logout)
return None
def logout_synchronous_other_backends(request, logout, django_sessions_keys):
backends = get_idp_backends()
if backends:
logger.info('backends %s' \
% str(backends))
else:
logger.info('no backends')
ok = True
for backend in backends:
ok = ok and backends.can_synchronous_logout(django_sessions_keys)
if not ok:
return return_logout_error(request, logout,
lasso.SAML2_STATUS_CODE_UNSUPPORTED_BINDING)
logger.info('treatments ended')
return None
def get_only_last_session(name_id, session_indexes, but_provider):
"""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.
"""
logger.debug('%s %s' % (name_id.dump(),
session_indexes))
lib_session1 = LibertySession.get_for_nameid_and_session_indexes(
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 = set([s.provider_id for s in lib_session])
result = []
for provider in providers:
if provider != but_provider:
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 = [u'<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(u'<NidAndSessionIndex ProviderID="{0.provider_id}" '
u'AssertionID="xxx" '
u'SessionIndex="{0.session_index}">'.format(liberty_session))
session.append(u'<saml:NameID Format="{0.name_id_format}" '.format(liberty_session))
if liberty_session.name_id_qualifier:
session.append(u'NameQualifier="{0.name_id_qualifier}" '.format(liberty_session))
if liberty_session.name_id_sp_name_qualifier:
session.append(u'SPNameQualifier="{0.name_id_sp_name_qualifier}" '.format(liberty_session))
session.append(u'>{0.name_id_content}</saml:NameID>'.format(liberty_session))
session.append(u'</NidAndSessionIndex>')
session.append(u'</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'''
logger.debug('lib_sessions %s' \
% lib_sessions)
session_dump = build_session_dump(lib_sessions).encode('utf8')
profile.setSessionFromDump(session_dump)
logger.debug('profile %s' \
% profile.session.dump())
@csrf_exempt
def slo_soap(request):
"""Endpoint for receiveing saml2:AuthnRequest by SOAP"""
message = get_soap_message(request)
if not message:
logger.error('no message received')
return HttpResponseBadRequest('Bad SOAP message')
logger.info('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.warn('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.error('No policy found for %s'\
% logout.remoteProviderId)
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
if not policy.accept_slo:
logger.warn('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.request.nameId,
logout.request.sessionIndexes, logout.remoteProviderId)
if not found:
logger.debug('no third SP session found')
else:
logger.info('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.error('slo cannot logout provider %s, it is '
'no more known.' % lib_session.provider_id)
continue
else:
logger.info('provider %s loaded' % str(p))
policy = get_sp_options_policy(p)
if not policy:
logger.error('No policy found for %s' \
% lib_session.provider_id)
elif not policy.forward_slo:
logger.info('%s configured to not reveive 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.error('one provider does \
not support SOAP %s' % [s.provider_id for s in lib_sessions])
except Exception, e:
logger.exception('slo, unknown error %s' % str(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.info('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.info('slo by SOAP')
soap_response = send_soap_request(request, logout)
logout.processResponseMsg(soap_response)
else:
logger.info('Provider does not support SOAP')
except lasso.Error:
logger.exception('slo, relaying to %s failed ' %
lib_session.provider_id)
'''
Respond to the SP initiating SLO
'''
try:
logout.buildResponseMsg()
except lasso.Error:
logger.exception('slo failure to build reponse msg')
raise NotImplementedError()
logger.info('processing finished')
logger.exception('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)
@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
logger.debug('asynchronous slo message %s' % message)
try:
provider = \
LibertyProvider.objects.get(entity_id=logout.remoteProviderId)
except ObjectDoesNotExist:
logger.warn('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.error('No policy found for %s'\
% logout.remoteProviderId)
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
if not policy.accept_slo:
logger.warn('received slo from %s not authorized'\
% logout.remoteProviderId)
return return_logout_error(request, logout,
AUTHENTIC_STATUS_CODE_UNAUTHORIZED)
try:
try:
logout.processRequestMsg(message)
except (lasso.ServerProviderNotFoundError,
lasso.ProfileUnknownProviderError), e:
load_provider(request, logout.remoteProviderId,
server=logout.server)
logout.processRequestMsg(message)
except lasso.DsError, e:
logger.exception('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 Exception, e:
logger.exception('slo %s' % message)
return error_page(_('Invalid logout request'), logger=logger)
session_indexes = logout.request.sessionIndexes
if len(session_indexes) == 0:
logger.error('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.info('asynchronous slo from %s' % logout.remoteProviderId)
# Filter sessions
all_sessions = LibertySession.get_for_nameid_and_session_indexes(
logout.request.nameId, logout.request.sessionIndexes)
# Does the request is valid ?
remote_provider_sessions = \
all_sessions.filter(provider_id=logout.remoteProviderId)
if not remote_provider_sessions.exists():
logger.error('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 = remote_provider_sessions.latest('creation')
set_session_dump_from_liberty_sessions(logout, [last_session])
try:
logout.validateRequest()
except:
logger.exception('slo error')
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, logout.dump(),
request.session.session_key)
return idp_views.redirect_to_logout(request, next_page='%s?id=%s' %
(reverse(finish_slo), urllib.quote(logout.request.id)))
def ko_icon(request):
return HttpResponseRedirect('%s/authentic2/images/ko.png' \
% settings.STATIC_URL)
def ok_icon(request):
return HttpResponseRedirect('%s/authentic2/images/ok.png' \
% settings.STATIC_URL)
@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.REQUEST.get('all')
next = request.REQUEST.get('next')
logger.debug('provider_id in parameter %s' % str(provider_id))
if request.method == 'GET' and 'provider_id' in request.GET:
provider_id = request.GET.get('provider_id')
logger.debug('provider_id from GET %s' % str(provider_id))
if request.method == 'POST' and 'provider_id' in request.POST:
provider_id = request.POST.get('provider_id')
logger.debug('provider_id from POST %s' % str(provider_id))
if not provider_id:
logger.info('to initiate a slo we need a provider_id')
return redirect_next(request, next) or ko_icon(request)
logger.info('slo initiated with %(provider_id)s' \
% {'provider_id': provider_id})
server = create_server(request)
logout = lasso.Logout(server)
provider = load_provider(request, provider_id, server=logout.server)
if not provider:
logger.error('slo failed to load provider')
policy = get_sp_options_policy(provider)
if not policy:
logger.error('No policy found for %s'\
% provider_id)
return redirect_next(request, next) or ko_icon(request)
if not policy.forward_slo:
logger.warn('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.error('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(map(lambda x: x.encode('utf8'),
session_indexes))
logout.msgRelayState = logout.request.id
try:
logout.buildRequestMsg()
except:
logger.exception('slo misc error')
return redirect_next(request, next) or ko_icon(request)
if logout.msgBody:
logger.info('slo by SOAP')
try:
soap_response = send_soap_request(request, logout)
except Exception, e:
logger.exception('slo SOAP failure due to %s' % str(e))
return redirect_next(request, next) or ko_icon(request)
return process_logout_response(request, logout, soap_response, next)
else:
logger.info('slo by redirect')
save_key_values(logout.request.id, logout.dump(), provider_id, next)
return HttpResponseRedirect(logout.msgUrl)
def process_logout_response(request, logout, soap_response, next):
logger.info('Response is %s' % str(soap_response))
try:
logout.processResponseMsg(soap_response)
except getattr(lasso, 'ProfileRequestDeniedError', lasso.LogoutRequestDeniedError):
logger.warning('logout request was denied')
return redirect_next(request, next) or ko_icon(request)
except:
logger.exception('\
slo error with soap response %r and logout dump %r' \
% (soap_response, logout.dump()))
else:
LibertySession.objects.filter(
django_session_key=request.session.session_key,
provider_id=logout.remoteProviderId).delete()
logger.info('deleted session to %s',
logout.remoteProviderId)
return redirect_next(request, next) or ok_icon(request)
def slo_return(request):
next = None
logger.info('return from redirect')
relay_state = request.REQUEST.get('RelayState')
if not relay_state:
logger.error('slo no relay state in response')
return error_page('Missing relay state', logger=logger)
else:
logger.debug('relay_state %s' % relay_state)
try:
logout_dump, provider_id, next = \
get_and_delete_key_values(relay_state)
except:
logger.exception('slo bad relay state in response')
return error_page('Bad relay state', logger=logger)
server = create_server(request)
logout = lasso.Logout.newFromDump(server, 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 = provider.service_provider.get_policy()
# FIXME: should use a logout_request_signature_check_hint
logout.setSignatureVerifyHint(policy.authn_request_signature_check_hint)
if not load_provider(request, provider_id, server=logout.server):
logger.error('slo failed to load provider')
return process_logout_response(request, logout,
get_saml2_query_request(request), next)
# Helpers
# SAMLv2 IdP settings variables
__local_options = getattr(settings, 'IDP_SAML2_METADATA_OPTIONS', {})
__user_backend_from_session = getattr(settings,
'IDP_SAML2_AUTHN_CONTEXT_FROM_SESSION', True)
__delta = getattr(settings, 'IDP_SECONDS_TOLERANCE', 60)
# Mapping to generate the metadata file, must be kept in sync with the url
# dispatcher
def get_provider_id_and_options(request, provider_id):
if not provider_id:
provider_id = reverse(metadata)
options = metadata_options
options.update(__local_options)
return provider_id, options
def get_metadata(request, provider_id=None):
'''Generate the metadata XML file
Metadata options can be overriden by setting IDP_METADATA_OPTIONS in
settings.py.
'''
provider_id, options = get_provider_id_and_options(request, provider_id)
return get_saml2_metadata(request, request.path, idp_map=metadata_map,
options=metadata_options)
__cached_server = None
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.
'''
global __cached_server
if __cached_server:
# clear loaded providers
__cached_server.providers = {}
return __cached_server
provider_id, options = get_provider_id_and_options(request, provider_id)
__cached_server = create_saml2_server(request, provider_id,
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.info('%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