authentic/src/authentic2/disco_service/disco_responder.py

237 lines
8.4 KiB
Python

# authentic2 - versatile identity manager
# Copyright (C) 2010-2019 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/>.
"""
Discovery Service Responder
See Identity Provider Discovery Service Protocol and Profile
OASIS Committee Specification 01
27 March 2008
"""
import logging
from xml.dom.minidom import parseString
from django.conf.urls import url
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlquote
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext as _
from authentic2 import settings
from authentic2.saml.common import error_page as base_error_page
from authentic2.saml.models import LibertyProvider
logger = logging.getLogger('authentic2.disco.responder')
def error_page(request, message, logger):
'''Customized disco service error page'''
message = u'disco: ' + message
return base_error_page(request, message, logger)
def save_key_values(request, *values):
request.session['save_key_values'] = values
def get_and_delete_key_values(request):
if 'save_key_values' in request.session:
return request.session['save_key_values']
return None
def set_or_refresh_prefered_idp(request, prefered_idp):
# XXX: Set cookie with the prefered idp entity ID
request.session['prefered_idp'] = prefered_idp
def get_prefered_idp(request):
# XXX: Read cookie if any
if 'prefered_idp' in request.session:
request.session['prefered_idp']
return None
def is_known_idp(idp):
# XXX: Check that the IdP selected is in the list of known idp
return True
def get_disco_return_url_from_metadata(entity_id):
liberty_provider = None
try:
liberty_provider = LibertyProvider.objects.get(entity_id=entity_id)
liberty_provider.service_provider
except Exception:
logger.warn('get_disco_return_url_from_metadata: unknown service provider %s', entity_id)
return None
dom = parseString(liberty_provider.metadata.encode('utf8'))
endpoints = dom.getElementsByTagNameNS(
'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol', 'DiscoveryResponse'
)
if not endpoints:
logger.warn('get_disco_return_url_from_metadata: no discovery service endpoint for %s', entity_id)
return None
ep = None
value = 0
first = True
# An endpoint is of type IndexedEndpointType
# Ignore malformed endpoint with no index
for endpoint in endpoints:
if 'index' in endpoint.attributes.keys():
if first:
ep = endpoint
value = int(endpoint.attributes['index'].value)
first = False
if int(endpoint.attributes['index'].value) < value:
value = int(endpoint.attributes['index'].value)
ep = endpoint
if not ep:
logger.warn("get_disco_return_url_from_metadata: no valid endpoint for %s", entity_id)
return None
logger.debug('get_disco_return_url_from_metadata: found endpoint with index %s', value)
if 'Location' in ep.attributes.keys():
location = ep.attributes['Location'].value
logger.debug('get_disco_return_url_from_metadata: location is %s', location)
return location
logger.warn('get_disco_return_url_from_metadata: no location found for endpoint with index %s', value)
return None
def is_param_id_in_return_url(return_url, returnIDParam):
url = urlparse.urlparse(return_url)
if url.query and returnIDParam in urlparse.parse_qs(url.query):
return True
return False
def add_param_to_url(url, param_name, value):
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
if query:
qs = urlparse.parse_qs(query)
qs[param_name] = [value]
query = urlparse.urlencode(qs)
else:
query = '%s=%s' % (param_name, value)
return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
def disco(request):
if not request.method == "GET":
message = _('HTTP verb not supported %s' % request.method)
return error_page(request, message, logger=logger)
entityID = None
_return = None
policy = ("urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single",)
returnIDParam = None
isPassive = False
# Back from the selection interface?
idp_selected = request.GET.get('idp_selected', '')
# Back from the selection interface
if idp_selected:
logger.info('disco: back from the idp selection interface with value %s', idp_selected)
if not is_known_idp(idp_selected):
message = 'The idp is unknown.'
logger.warn("disco: Unknown selected idp %s" % idp_selected)
save_key_values(request, entityID, _return, policy, returnIDParam, isPassive)
return HttpResponseRedirect(reverse(idp_selection))
entityID, _return, policy, returnIDParam, isPassive = get_and_delete_key_values(request)
# Not back from the selection interface
else:
# Discovery request parameters
entityID = request.GET.get('entityID', '')
_return = request.GET.get('return', '')
policy = request.GET.get(
'idp_selected', 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single'
)
returnIDParam = request.GET.get('returnIDParam', 'entityID')
# XXX: isPassive is unused
isPassive = request.GET.get('isPassive', '')
if isPassive and isPassive == 'true':
isPassive = True
else:
isPassive = False
if not entityID:
message = _('missing mandatory parameter entityID')
return error_page(request, message, logger=logger)
if policy != 'urn:oasis:names:tc:SAML:profiles:SSO:idp-discovery-protocol:single':
message = _('policy %r not implemented') % policy
return error_page(request, message, logger=logger)
# If we use metadata, we ignore the parameter return and take it from the
# md. Else and if no return parameter in query, it is an unconformant SP.
return_url = None
if getattr(settings, 'USE_OF_METADATA', True):
return_url = get_disco_return_url_from_metadata(entityID)
else:
return_url = _return
if not return_url:
message = _('unable to find a valid return url for %s') % entityID
return error_page(request, message, logger=logger)
# Check that the return_url does not already contain a param with name
# equal to returnIDParam. Else, it is an unconformant SP.
if is_param_id_in_return_url(return_url, returnIDParam):
message = _('invalid return url %(return_url)s for %(entity_id)s') % dict(
return_url=return_url, entity_id=entityID
)
return error_page(request, message, logger=logger)
# not back from selection interface
if not idp_selected:
idp_selected = get_prefered_idp(request)
# not back from selection interface and no registered prefered idp
if not idp_selected:
# no idp selected and we must not interect with the user
if isPassive:
# No IdP selected = just return to the return url
return HttpResponseRedirect(return_url)
# Go to selection interface
else:
save_key_values(request, entityID, _return, policy, returnIDParam, isPassive)
return HttpResponseRedirect(reverse(idp_selection))
# We got it!
set_or_refresh_prefered_idp(request, idp_selected)
return HttpResponseRedirect(add_param_to_url(return_url, returnIDParam, idp_selected))
def idp_selection(request):
# XXX: Code here the IdP selection
idp_selected = urlquote('http://www.identity-hub.com/idp/saml2/metadata')
return HttpResponseRedirect('%s?idp_selected=%s' % (reverse(disco), idp_selected))
urlpatterns = [
url(r'^disco$', disco),
url(r'^idp_selection$', idp_selection),
]