Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Merge in my other PR about failing back to supported idp bindings, an…
…d don't create two requests in the case where we use a custom template for http-post binding.
  • Loading branch information
Reece authored and Reece committed Apr 12, 2017
commit 501095a6419048eb5371334b143c37ff48365fd8
24 changes: 15 additions & 9 deletions djangosaml2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

from django.conf import settings
from saml2.s_utils import UnknownSystemEntity


def get_custom_setting(name, default=None):
Expand All @@ -33,12 +34,17 @@ def available_idps(config, langpref=None):
return dict([(idp, config.metadata.name(idp, langpref)) for idp in idps])


def get_location(http_info):
"""Extract the redirect URL from a pysaml2 http_info object"""
assert 'headers' in http_info
headers = http_info['headers']

assert len(headers) == 1
header_name, header_value = headers[0]
assert header_name == 'Location'
return header_value
def get_idp_sso_supported_bindings(idp_entity_id=None):
"""Returns the list of bindings supported by an IDP
This is not clear in the pysaml2 code, so wrapping it in a util"""
# avoid circular import
from djangosaml2.conf import get_config
# load metadata store from config
config = get_config()
meta = getattr(config, 'metadata', {})
# if idp is None, assume only one exists so just use that
idp_entity_id = available_idps(config).keys().pop()
try:
return meta.service(idp_entity_id, 'idpsso_descriptor', 'single_sign_on_service').keys()
except UnknownSystemEntity:
return []
86 changes: 62 additions & 24 deletions djangosaml2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ def csrf_exempt(view_func):
from saml2.client import Saml2Client
from saml2.metadata import entity_descriptor
from saml2.ident import code, decode
from saml2.s_utils import UnsupportedBinding
from saml2.sigver import MissingKey
from saml2.response import StatusError

from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
from djangosaml2.cache import StateCache
from djangosaml2.conf import get_config
from djangosaml2.signals import post_authenticated
from djangosaml2.utils import get_custom_setting, available_idps, get_location
from djangosaml2.utils import get_custom_setting, available_idps, get_idp_sso_supported_bindings, get_location


logger = logging.getLogger('djangosaml2')
Expand Down Expand Up @@ -154,43 +155,80 @@ def login(request,
#
# Read more in the official SAML2 specs (3.4.4.1):
# http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
binding = BINDING_HTTP_POST if getattr(conf, '_sp_authn_requests_signed', False) else BINDING_HTTP_REDIRECT
sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT

# ensure our selected binding is supported by the IDP
supported_bindings = get_idp_sso_supported_bindings(selected_idp)
if binding not in supported_bindings:
logger.debug('Binding %s not in IDP %s supported bindings: %s',
binding, selected_idp, supported_bindings)
if binding == BINDING_HTTP_POST:
logger.warning('IDP %s does not support %s, trying %s',
selected_idp, binding, BINDING_HTTP_REDIRECT)
binding = BINDING_HTTP_REDIRECT
if sign_requests:
logger.warning('sp_authn_requests_signed is True, but ignoring because pysaml2 does not support it for %s', BINDING_HTTP_REDIRECT)
else:
binding = BINDING_HTTP_POST
# if switched binding still not supported, give up
if binding not in supported_bindings:
raise UnsupportedBinding('IDP does not support %s or %s' % (
BINDING_HTTP_POST, BINDING_HTTP_REDIRECT))

client = Saml2Client(conf)
try:
(session_id, result) = client.prepare_for_authenticate(
entityid=selected_idp, relay_state=came_from,
binding=binding,
)
except TypeError as e:
logger.error('Unable to know which IdP to use')
return HttpResponse(text_type(e))

logger.debug('Saving the session_id in the OutstandingQueries cache')
oq_cache = OutstandingQueriesCache(request.session)
oq_cache.set(session_id, came_from)

logger.debug('Redirecting user to the IdP via %s binding.', binding.split(':')[-1])
http_response = None
logger.debug('Redirecting user to the IdP via %s binding.', binding)
if binding == BINDING_HTTP_REDIRECT:
return HttpResponseRedirect(get_location(result))
try:
# we use sign kwarg to override in case of redirect binding
# otherwise pysaml2 may sign the xml for redirect which is incorrect
session_id, result = client.prepare_for_authenticate(
entityid=selected_idp, relay_state=came_from,
binding=binding, sign=False)
except TypeError as e:
logger.error('Unable to know which IdP to use')
return HttpResponse(text_type(e))
else:
http_response = HttpResponseRedirect(get_location(result))
elif binding == BINDING_HTTP_POST:
# use the html provided by pysaml2 if no template specified
if not post_binding_form_template:
# use the html provided by pysaml2
return HttpResponse(result['data'])
try:
session_id, result = client.prepare_for_authenticate(
entityid=selected_idp, relay_state=came_from,
binding=binding)
except TypeError as e:
logger.error('Unable to know which IdP to use')
return HttpResponse(text_type(e))
else:
http_response = HttpResponse(result['data'])
# get request XML to build our own html based on the template
else:
# manually get request XML to build our own template
request_id, request_xml = client.create_authn_request(
client.sso_location(selected_idp, binding),
try:
location = client.sso_location(selected_idp, binding)
except TypeError as e:
logger.error('Unable to know which IdP to use')
return HttpResponse(text_type(e))
session_id, request_xml = client.create_authn_request(
location,
binding=binding)
return render(request, post_binding_form_template, {
'target_url': result['url'],
http_response = render(request, post_binding_form_template, {
'target_url': location,
'params': {
'SAMLRequest': base64.b64encode(request_xml),
'RelayState': came_from,
},
})
else:
raise NotImplementedError('Unsupported binding: %s', binding)
raise UnsupportedBinding('Unsupported binding: %s', binding)

# success, so save the session ID and return our response
logger.debug('Saving the session_id in the OutstandingQueries cache')
oq_cache = OutstandingQueriesCache(request.session)
oq_cache.set(session_id, came_from)
return http_response


@require_POST
Expand Down