Beispiel #1
0
class TestSamlMixins:
    """
    This class contains contains methods copied from djangosaml2.tests.SAML2Tests.
    """
    def b64_for_post(self, xml_text, encoding='utf-8'):
        return base64.b64encode(xml_text.encode(encoding)).decode('ascii')

    def add_outstanding_query(self, session_id, came_from):
        settings.SESSION_ENGINE = 'django.contrib.sessions.backends.db'
        engine = import_string(settings.SESSION_ENGINE)
        self.saml_session = engine.SessionStore()
        self.saml_session.save()
        self.oq_cache = OutstandingQueriesCache(self.saml_session)

        self.oq_cache.set(
            session_id if isinstance(session_id, str) else session_id.decode(),
            came_from,
        )
        self.saml_session.save()
        self.client.cookies[
            settings.SESSION_COOKIE_NAME] = self.saml_session.session_key
Beispiel #2
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html',
          post_binding_form_template='djangosaml2/post_binding_form.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.

    * post_binding_form_template - path to a template containing HTML form with
    hidden input elements, used to send the SAML message data when HTTP POST
    binding is being used. You can customize this template to include custom
    branding and/or text explaining the automatic redirection process. Please
    see the example template in
    templates/djangosaml2/example_post_binding_form.html
    If set to None or nonexistent template, default form from the saml2 library
    will be rendered.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if not request.user.is_anonymous():
        try:
            redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
        except AttributeError:
            redirect_authenticated_user = True

        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render_to_response(authorization_error_template, {
                    'came_from': came_from,
                    }, context_instance=RequestContext(request))

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render_to_response(wayf_template, {
                'available_idps': idps.items(),
                'came_from': came_from,
                }, context_instance=RequestContext(request))

    # Choose binding (REDIRECT vs. POST).
    # When authn_requests_signed is turned on, HTTP Redirect binding cannot be
    # used the same way as without signatures; proper usage in this case involves
    # stripping out the signature from SAML XML message and creating a new
    # signature, following precise steps defined in the SAML2.0 standard.
    #
    # It is not feasible to implement this since we wouldn't be able to use an
    # external (xmlsec1) library to handle the signatures - more (higher level)
    # context is needed in order to create such signature (like the value of
    # RelayState parameter).
    #
    # Therefore it is much easier to use the HTTP POST binding in this case, as
    # it can relay the whole signed SAML message as is, without the need to
    # manipulate the signature or the XML message itself.
    #
    # 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

    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(unicode(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])
    if binding == BINDING_HTTP_REDIRECT:
        return HttpResponseRedirect(get_location(result))
    elif binding == BINDING_HTTP_POST:
        if not post_binding_form_template:
            return HttpResponse(result['data'])
        try:
            params = get_hidden_form_inputs(result['data'][3])
            return render_to_response(post_binding_form_template, {
                    'target_url': result['url'],
                    'params': params,
                    }, context_instance=RequestContext(request))
        except TemplateDoesNotExist:
            return HttpResponse(result['data'])
    else:
        raise NotImplementedError('Unsupported binding: %s', binding)
Beispiel #3
0
 def add_outstanding_query(self, session_id, came_from):
     session = self.client.session
     oq_cache = OutstandingQueriesCache(session)
     oq_cache.set(session_id, came_from)
     session.save()
     self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
Beispiel #4
0
def spid_login(
    request,
    config_loader_path=None,
    wayf_template="wayf.html",
    authorization_error_template="djangosaml2/auth_error.html",
):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http POST protocol binding.
    """
    logger.debug("SPID Login process started")

    next_url = request.GET.get("next", settings.LOGIN_REDIRECT_URL)
    if not next_url:
        logger.warning("The next parameter exists but is empty")
        next_url = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not validate_referral_url(request, next_url):
        next_url = settings.LOGIN_REDIRECT_URL

    if request.user.is_authenticated:
        redirect_authenticated_user = getattr(
            settings, "SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN", True)
        if redirect_authenticated_user:
            return HttpResponseRedirect(next_url)
        else:  # pragma: no cover
            logger.debug("User is already logged in")
            return render(request, authorization_error_template,
                          {"came_from": next_url})

    # this works only if request came from wayf
    selected_idp = request.GET.get("idp", None)

    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug("A discovery process is needed")
        return render(
            request,
            wayf_template,
            {
                "available_idps": idps.items(),
                "next_url": next_url
            },
        )
    else:
        # otherwise is the first one
        _msg = "Unable to know which IdP to use"
        try:
            selected_idp = selected_idp or list(idps.keys())[0]
        except TypeError as e:  # pragma: no cover
            logger.error(f"{_msg}: {e}")
            return HttpResponseNotFound(_msg)
        except IndexError as e:  # pragma: no cover
            logger.error(f"{_msg}: {e}")
            return HttpResponseNotFound(_msg)

    # ensure our selected binding is supported by the IDP
    logger.debug(
        f"Trying binding {SAML2_DEFAULT_BINDING} for IDP {selected_idp}")
    supported_bindings = get_idp_sso_supported_bindings(selected_idp,
                                                        config=conf)
    if not supported_bindings:
        _msg = "IdP Metadata not found or not valid"
        return HttpResponseNotFound(_msg)

    if SAML2_DEFAULT_BINDING not in supported_bindings:
        _msg = (f"Requested: {SAML2_DEFAULT_BINDING} but the selected "
                f"IDP [{selected_idp}] doesn't support "
                f"{BINDING_HTTP_POST} or {BINDING_HTTP_REDIRECT}. "
                f"Check if IdP Metadata is correctly loaded and updated.")
        logger.error(_msg)
        raise UnsupportedBinding(_msg)

    # SPID things here
    try:
        login_response = spid_sp_authn_request(conf, selected_idp, next_url)
    except UnknownSystemEntity as e:  # pragma: no cover
        _msg = f"Unknown IDP Entity ID: {selected_idp}"
        logger.error(f"{_msg}: {e}")
        return HttpResponseNotFound(_msg)

    session_id = login_response["session_id"]
    http_response = login_response["http_response"]

    # success, so save the session ID and return our response
    logger.debug(
        f"Saving session-id {session_id} in the OutstandingQueries cache")
    oq_cache = OutstandingQueriesCache(request.saml_session)
    oq_cache.set(session_id, next_url)

    if SAML2_DEFAULT_BINDING == saml2.BINDING_HTTP_POST:
        return HttpResponse(http_response["data"])
    elif SAML2_DEFAULT_BINDING == saml2.BINDING_HTTP_REDIRECT:
        headers = dict(login_response["http_response"]["headers"])
        return HttpResponseRedirect(headers["Location"])
Beispiel #5
0
                                  context_instance=RequestContext(request))

    client = Saml2Client(conf)
    try:
        (session_id, result) = client.prepare_for_authenticate(
            entityid=selected_idp,
            relay_state=came_from,
            binding=BINDING_HTTP_REDIRECT,
        )
    except TypeError, e:
        logger.error('Unable to know which IdP to use')
        return HttpResponse(unicode(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 the user to the IdP')
    return HttpResponseRedirect(get_location(result))


@require_POST
@csrf_exempt
def assertion_consumer_service(request,
                               config_loader_path=None,
                               attribute_mapping=None,
                               create_unknown_user=None):
    """SAML Authorization Response endpoint

    The IdP will send its response to this view, which
    will process it with pysaml2 help and log the user
Beispiel #6
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html',
          post_binding_form_template='djangosaml2/post_binding_form.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.

    * post_binding_form_template - path to a template containing HTML form with
    hidden input elements, used to send the SAML message data when HTTP POST
    binding is being used. You can customize this template to include custom
    branding and/or text explaining the automatic redirection process. Please
    see the example template in
    templates/djangosaml2/example_post_binding_form.html
    If set to None or nonexistent template, default form from the saml2 library
    will be rendered.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not is_safe_url(url=came_from, host=request.get_host()):
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if not request.user.is_anonymous():
        try:
            redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
        except AttributeError:
            redirect_authenticated_user = True

        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render(request, authorization_error_template, {
                'came_from': came_from,
            })

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render(request, wayf_template, {
            'available_idps': idps.items(),
            'came_from': came_from,
        })

    # choose a binding to try first
    sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
    binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT
    logger.debug('Trying binding %s for IDP %s', binding, selected_idp)

    # ensure our selected binding is supported by the IDP
    supported_bindings = get_idp_sso_supported_bindings(selected_idp,
                                                        config=conf)
    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
        else:
            logger.warning('IDP %s does not support %s,  trying %s',
                           selected_idp, binding, BINDING_HTTP_POST)
            binding = BINDING_HTTP_POST
        # if switched binding still not supported, give up
        if binding not in supported_bindings:
            raise UnsupportedBinding('IDP %s does not support %s or %s',
                                     selected_idp, BINDING_HTTP_POST,
                                     BINDING_HTTP_REDIRECT)

    client = Saml2Client(conf)
    http_response = None

    logger.debug('Redirecting user to the IdP via %s binding.', binding)
    if binding == BINDING_HTTP_REDIRECT:
        try:
            # do not sign the xml itself, instead us the sigalg to
            # generate the signature as a URL param
            sigalg = SIG_RSA_SHA1 if sign_requests else None
            session_id, result = client.prepare_for_authenticate(
                entityid=selected_idp,
                relay_state=came_from,
                binding=binding,
                sign=False,
                sigalg=sigalg)
        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:
            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:
            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)
            http_response = render(
                request, post_binding_form_template, {
                    'target_url': location,
                    'params': {
                        'SAMLRequest': base64.b64encode(
                            binary_type(request_xml)),
                        'RelayState': came_from,
                    },
                })
    else:
        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
Beispiel #7
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if not request.user.is_anonymous():
        try:
            redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
        except AttributeError:
            redirect_authenticated_user = True

        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render_to_response(authorization_error_template, {
                'came_from': came_from,
            },
                                      context_instance=RequestContext(request))

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render_to_response(wayf_template, {
            'available_idps': idps.items(),
            'came_from': came_from,
        },
                                  context_instance=RequestContext(request))

    client = Saml2Client(conf)
    try:
        (session_id, result) = client.prepare_for_authenticate(
            entityid=selected_idp,
            relay_state=came_from,
            binding=BINDING_HTTP_REDIRECT,
        )
    except TypeError as e:
        logger.error('Unable to know which IdP to use')
        return HttpResponse(str(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 the user to the IdP')
    return HttpResponseRedirect(get_location(result))
Beispiel #8
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html',
          post_binding_form_template='djangosaml2/post_binding_form.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.

    * post_binding_form_template - path to a template containing HTML form with
    hidden input elements, used to send the SAML message data when HTTP POST
    binding is being used. You can customize this template to include custom
    branding and/or text explaining the automatic redirection process. Please
    see the example template in
    templates/djangosaml2/example_post_binding_form.html
    If set to None or nonexistent template, default form from the saml2 library
    will be rendered.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not is_safe_url(url=came_from, host=request.get_host()):
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if not request.user.is_anonymous():
        try:
            redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
        except AttributeError:
            redirect_authenticated_user = True

        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render(request, authorization_error_template, {
                'came_from': came_from,
            })

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render(request, wayf_template, {
            'available_idps': idps.items(),
            'came_from': came_from,
        })

    # Choose binding (REDIRECT vs. POST).
    # When authn_requests_signed is turned on, HTTP Redirect binding cannot be
    # used the same way as without signatures; proper usage in this case involves
    # stripping out the signature from SAML XML message and creating a new
    # signature, following precise steps defined in the SAML2.0 standard.
    #
    # It is not feasible to implement this since we wouldn't be able to use an
    # external (xmlsec1) library to handle the signatures - more (higher level)
    # context is needed in order to create such signature (like the value of
    # RelayState parameter).
    #
    # Therefore it is much easier to use the HTTP POST binding in this case, as
    # it can relay the whole signed SAML message as is, without the need to
    # manipulate the signature or the XML message itself.
    #
    # 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

    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(unicode(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])
    if binding == BINDING_HTTP_REDIRECT:
        return HttpResponseRedirect(get_location(result))
    elif binding == BINDING_HTTP_POST:
        if not post_binding_form_template:
            return HttpResponse(result['data'])
        try:
            params = get_hidden_form_inputs(result['data'][3])
            return render(request, post_binding_form_template, {
                'target_url': result['url'],
                'params': params,
            })
        except (DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden):
            raise PermissionDenied
        except TemplateDoesNotExist:
            return HttpResponse(result['data'])
    else:
        raise NotImplementedError('Unsupported binding: %s', binding)
Beispiel #9
0
def spid_login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http POST protocol binding.
    """
    logger.debug('SPID Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not is_safe_url_compat(url=came_from, allowed_hosts={request.get_host()}):
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if callable(request.user.is_authenticated):
        redirect_authenticated_user = getattr(settings,
                                              'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN',
                                              True)
        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render(request, authorization_error_template, {
                    'came_from': came_from})

    # this works only if request came from wayf
    selected_idp = request.GET.get('idp', None)

    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render(request, wayf_template, {
                'available_idps': idps.items(),
                'came_from': came_from})
    else:
        # otherwise is the first one
        try:
            selected_idp = list(idps.keys())[0]
        except TypeError as e:
            logger.error('Unable to know which IdP to use')
            return HttpResponse(text_type(e))

    # choose a binding to try first
    # sign_requests = getattr(conf, '_sp_authn_requests_signed', False)

    binding = BINDING_HTTP_POST
    logger.debug('Trying binding %s for IDP %s', binding, selected_idp)

    # ensure our selected binding is supported by the IDP
    supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf)
    if binding != BINDING_HTTP_POST:
            raise UnsupportedBinding('IDP %s does not support %s or %s',
                                     selected_idp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT)

    client = Saml2Client(conf)

    logger.debug('Redirecting user to the IdP via %s binding.', binding)
    # use the html provided by pysaml2 if no template was specified or it didn't exist
    # SPID want the fqdn of the IDP, not the SSO endpoint
    location_fixed = selected_idp
    location = client.sso_location(selected_idp, binding)
    # ...hope to see the SSO endpoint soon in spid-testenv2

    authn_req = saml2.samlp.AuthnRequest()
    authn_req.destination = location_fixed
    # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...)
    authn_req.attribute_consuming_service_index = "0"

    # import pdb; pdb.set_trace()
    issuer = saml2.saml.Issuer()
    issuer.name_qualifier = client.config.entityid
    issuer.text = client.config.entityid
    issuer.format = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
    authn_req.issuer = issuer

    # message id
    authn_req.id = saml2.s_utils.sid()
    authn_req.version = saml2.VERSION # "2.0"
    authn_req.issue_instant = saml2.time_util.instant()

    name_id_policy = saml2.samlp.NameIDPolicy()
    # del(name_id_policy.allow_create)
    name_id_policy.format = settings.SPID_NAMEID_FORMAT
    authn_req.name_id_policy  = name_id_policy

    authn_context = requested_authn_context(class_ref=settings.SPID_AUTH_CONTEXT)
    authn_req.requested_authn_context = authn_context

    authn_req.protocol_binding = settings.SPID_DEFAULT_BINDING

    assertion_consumer_service_url = client.config._sp_endpoints['assertion_consumer_service'][0][0]
    authn_req.assertion_consumer_service_url = assertion_consumer_service_url #'http://sp1.testunical.it:8000/saml2/acs/'

    authn_req_signed = client.sign(authn_req, sign_prepare=False,
                                   sign_alg=settings.SPID_ENC_ALG,
                                   digest_alg=settings.SPID_DIG_ALG)
    session_id = authn_req.id

    _req_str = authn_req_signed
    logger.debug('AuthRequest to {}: {}'.format(selected_idp, (_req_str)))
    http_info = client.apply_binding(binding,
                                     _req_str, location,
                                     sign=True,
                                     sigalg=settings.SPID_ENC_ALG)

    # 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 HttpResponse(http_info['data'])
Beispiel #10
0
def spid_login(request,
               config_loader_path=None,
               wayf_template='wayf.html',
               authorization_error_template='djangosaml2/auth_error.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http POST protocol binding.
    """

    logger.debug('SPID Login process started')
    next_url = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not next_url:
        logger.warning('The next parameter exists but is empty')
        next_url = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not validate_referral_url(request, next_url):
        next_url = settings.LOGIN_REDIRECT_URL

    if callable(request.user.is_authenticated):
        redirect_authenticated_user = getattr(
            settings, 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN', True)
        if redirect_authenticated_user:
            return HttpResponseRedirect(next_url)
        else:
            logger.debug('User is already logged in')
            return render(request, authorization_error_template,
                          {'came_from': next_url})

    # this works only if request came from wayf
    selected_idp = request.GET.get('idp', None)

    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render(request, wayf_template, {
            'available_idps': idps.items(),
            'next_url': next_url
        })
    else:
        # otherwise is the first one
        try:
            selected_idp = selected_idp or list(idps.keys())[0]
        except TypeError as e:
            logger.error('Unable to know which IdP to use')
            return HttpResponse(text_type(e))

    binding = BINDING_HTTP_POST
    logger.debug(f'Trying binding {binding} for IDP {selected_idp}')

    # ensure our selected binding is supported by the IDP
    supported_bindings = get_idp_sso_supported_bindings(selected_idp,
                                                        config=conf)
    if binding != BINDING_HTTP_POST:
        raise UnsupportedBinding('IDP %s does not support %s or %s',
                                 selected_idp, BINDING_HTTP_POST,
                                 BINDING_HTTP_REDIRECT)

    # SPID things here
    login_response = spid_sp_authn_request(conf, selected_idp, binding,
                                           settings.SPID_NAMEID_FORMAT,
                                           settings.SPID_AUTH_CONTEXT,
                                           settings.SPID_SIG_ALG,
                                           settings.SPID_DIG_ALG, next_url)

    session_id = login_response['session_id']
    http_response = login_response['http_response']

    # success, so save the session ID and return our response
    logger.debug(
        f'Saving session-id {session_id} in the OutstandingQueries cache')
    oq_cache = OutstandingQueriesCache(request.saml_session)
    oq_cache.set(session_id, next_url)
    return HttpResponse(http_response['data'])
    def test_valid_form(self):
        url, data, session_id = self._get_sp_authn_request()
        response = self.client.post(url, data, follow=True)
        # csrf_regexp = '<input type="hidden" name="csrfmiddlewaretoken" value="(?P<value>[a-zA-Z0-9+=]*)">'
        # login_data['csrfmiddlewaretoken'] = re.findall(csrf_regexp, response.content.decode())[0]
        self.login_data['password'] = '******'
        login_response = self.client.post(login_url,
                                          data=self.login_data,
                                          follow=True)
        # is there a SAML response?
        saml_resp = re.findall(samlresponse_form_regexp,
                               login_response.content.decode())
        assert saml_resp

        # test agreement screens
        self.sp.agreement_screen = 1
        self.sp.save()
        login_response = self.client.post(login_url,
                                          data=self.login_data,
                                          follow=True)
        assert 'has requested the following informations' in login_response.content.decode(
        )

        # don't show again
        agr_data = dict(dont_show_again=1, confirm=1)
        agr_url = reverse('uniauth:saml_user_agreement')
        agr_response = self.client.post(agr_url, data=agr_data, follow=True)

        # login again, agreement screen should not be displayed anymore
        # purge persistent_id from storage
        self.user.persistentid_set.all().delete()
        login_response = self.client.post(login_url,
                                          data=self.login_data,
                                          follow=True)
        saml_resp = re.findall(samlresponse_form_regexp,
                               login_response.content.decode())
        assert saml_resp

        # transient name_id format, remove persistent_id
        sp_conf = copy.deepcopy(SAML_SP_CONFIG)
        del (sp_conf['service']['sp']['name_id_format'][0])
        self.sp_conf.load(sp_conf)
        url, data, session_id = self._get_sp_authn_request()
        response = self.client.post(url, data, follow=True)
        login_response = self.client.post(login_url,
                                          data=self.login_data,
                                          follow=True)

        # test logout
        session = self.client.session
        state = StateCache(session)
        identity_cache = IdentityCache(session)
        oq_cache = OutstandingQueriesCache(session)
        oq_cache.set(session_id, '/')
        outstanding_queries = oq_cache.outstanding_queries()
        client = Saml2Client(self.sp_conf,
                             state_cache=state,
                             identity_cache=IdentityCache(session))
        response = client.parse_authn_request_response(saml_resp[0],
                                                       BINDING_HTTP_POST,
                                                       outstanding_queries)
        # this should take name_id dict
        # result = client.global_logout(session['SAML']['subject_id'])
        logout_result = client.global_logout(response.name_id)

        # is there a SAML response?
        saml_req_logout = re.findall(samlrequest_form_regexp,
                                     logout_result[idp_eid][1]['data'])
        assert saml_req_logout

        logout_response = self.client.post(
            logout_url, data={'SAMLRequest': saml_req_logout}, follow=True)
Beispiel #12
0
class SAML2Tests(TestCase):

    urls = 'djangosaml2.tests.urls'

    def init_cookies(self):
        self.client.cookies[settings.SESSION_COOKIE_NAME] = 'testing'

    def add_outstanding_query(self, session_id, came_from):
        settings.SESSION_ENGINE = 'django.contrib.sessions.backends.db'
        engine = import_module(settings.SESSION_ENGINE)
        self.saml_session = engine.SessionStore()
        self.saml_session.save()
        self.oq_cache = OutstandingQueriesCache(self.saml_session)

        self.oq_cache.set(
            session_id if isinstance(session_id, str) else session_id.decode(),
            came_from)
        self.saml_session.save()
        self.client.cookies[
            settings.SESSION_COOKIE_NAME] = self.saml_session.session_key

    def b64_for_post(self, xml_text, encoding='utf-8'):
        return base64.b64encode(xml_text.encode(encoding)).decode('ascii')

    def test_get_idp_sso_supported_bindings_noargs(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        idp_id = 'https://idp.example.com/simplesaml/saml2/idp/metadata.php'
        self.assertEqual(
            get_idp_sso_supported_bindings()[0],
            list(settings.SAML_CONFIG['service']['sp']['idp'][idp_id]
                 ['single_sign_on_service'].keys())[0])

    def test_get_idp_sso_supported_bindings_unknown_idp(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        self.assertEqual(
            get_idp_sso_supported_bindings(idp_entity_id='random'), [])

    def test_get_idp_sso_supported_bindings_no_idps(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=[],
            metadata_file='remote_metadata_no_idp.xml',
        )
        with self.assertRaisesMessage(ImproperlyConfigured,
                                      "No IdP configured!"):
            get_idp_sso_supported_bindings()

    def test_unsigned_post_authn_request(self):
        """
        Test that unsigned authentication requests via POST binding
        does not error.

        https://github.com/knaperek/djangosaml2/issues/168
        """
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_post_binding.xml',
            authn_requests_signed=False)
        response = self.client.get(reverse('saml2_login'))

        self.assertEqual(response.status_code, 200)

        # Using POST-binding returns a page with form containing the SAMLRequest
        response_parser = SAMLPostFormParser()
        response_parser.feed(response.content.decode('utf-8'))
        saml_request = response_parser.saml_request_value

        self.assertIsNotNone(saml_request)
        self.assertIn('AuthnRequest xmlns',
                      base64.b64decode(saml_request).decode('utf-8'))

    def test_login_evil_redirect(self):
        """
        Make sure that if we give an URL other than our own host as the next
        parameter, it is replaced with the default LOGIN_REDIRECT_URL.
        """

        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        response = self.client.get(
            reverse('saml2_login') + '?next=http://evil.com')
        url = urlparse(response['Location'])
        params = parse_qs(url.query)

        self.assertEqual(params['RelayState'], [
            settings.LOGIN_REDIRECT_URL,
        ])

    def test_no_redirect(self):
        """
        Make sure that if we give an empty path as the next parameter,
        it is replaced with the default LOGIN_REDIRECT_URL.
        """

        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        response = self.client.get(reverse('saml2_login') + '?next=')
        url = urlparse(response['Location'])
        params = parse_qs(url.query)

        self.assertEqual(params['RelayState'], [
            settings.LOGIN_REDIRECT_URL,
        ])

    def test_login_one_idp(self):
        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        response = self.client.get(reverse('saml2_login'))
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)
        self.assertIn('RelayState', params)

        saml_request = params['SAMLRequest'][0]
        self.assertIn('AuthnRequest xmlns',
                      decode_base64_and_inflate(saml_request).decode('utf-8'))

        # if we set a next arg in the login view, it is preserverd
        # in the RelayState argument
        nexturl = '/another-view/'
        response = self.client.get(reverse('saml2_login'), {'next': nexturl})
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)
        self.assertIn('RelayState', params)
        self.assertEqual(params['RelayState'][0], nexturl)

    def test_login_several_idps(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=[
                'idp1.example.com', 'idp2.example.com', 'idp3.example.com'
            ],
            metadata_file='remote_metadata_three_idps.xml',
        )
        response = self.client.get(reverse('saml2_login'))
        # a WAYF page should be displayed
        self.assertContains(response, 'Where are you from?', status_code=200)
        for i in range(1, 4):
            link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/'
            self.assertContains(response, link % i)

        # click on the second idp
        response = self.client.get(
            reverse('saml2_login'), {
                'idp':
                'https://idp2.example.com/simplesaml/saml2/idp/metadata.php',
                'next': '/',
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp2.example.com')
        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)
        self.assertIn('RelayState', params)

        saml_request = params['SAMLRequest'][0]
        self.assertIn('AuthnRequest xmlns',
                      decode_base64_and_inflate(saml_request).decode('utf-8'))

    def test_assertion_consumer_service(self):
        # Get initial number of users
        initial_user_count = User.objects.count()
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        response = self.client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)
        # session_id should start with a letter since it is a NCName
        came_from = '/another-view/'
        self.add_outstanding_query(session_id, came_from)

        # this will create a user
        saml_response = auth_response(session_id, 'student')
        _url = reverse('saml2_acs')
        response = self.client.post(
            _url, {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']
        url = urlparse(location)
        self.assertEqual(url.path, came_from)

        self.assertEqual(User.objects.count(), initial_user_count + 1)
        user_id = self.client.session[SESSION_KEY]
        user = User.objects.get(id=user_id)
        self.assertEqual(user.username, 'student')

        # let's create another user and log in with that one
        new_user = User.objects.create(username='******', password='******')

        #  session_id = "a1111111111111111111111111111111"
        client = Client()
        response = client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)

        came_from = ''  # bad, let's see if we can deal with this
        saml_response = auth_response(session_id, 'teacher')
        self.add_outstanding_query(session_id, '/')
        response = client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        # as the RelayState is empty we have redirect to LOGIN_REDIRECT_URL
        self.assertEqual(url.path, settings.LOGIN_REDIRECT_URL)
        self.assertEqual(force_text(new_user.id), client.session[SESSION_KEY])

    def test_assertion_consumer_service_already_logged_in_allowed(self):
        self.client.force_login(
            User.objects.create(username='******', password='******'))

        settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN = True

        came_from = '/dummy-url/'
        response = self.client.get(
            reverse('saml2_login') + f'?next={came_from}')
        self.assertEqual(response.status_code, 302)
        url = urlparse(response['Location'])
        self.assertEqual(url.path, came_from)

    def test_assertion_consumer_service_already_logged_in_error(self):
        self.client.force_login(
            User.objects.create(username='******', password='******'))

        settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN = False

        came_from = '/dummy-url/'
        response = self.client.get(
            reverse('saml2_login') + f'?next={came_from}')
        self.assertEqual(response.status_code, 200)
        self.assertInHTML(
            "<p>You are already logged in and you are trying to go to the login page again.</p>",
            response.content.decode())

    def test_assertion_consumer_service_no_session(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        response = self.client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)
        # session_id should start with a letter since it is a NCName

        came_from = '/another-view/'
        self.add_outstanding_query(session_id, came_from)

        # Authentication is confirmed.
        saml_response = auth_response(session_id, 'student')
        response = self.client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']
        url = urlparse(location)
        self.assertEqual(url.path, came_from)

        # Session should no longer be in outstanding queries.
        saml_response = auth_response(session_id, 'student')
        response = self.client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 403)

    def test_missing_param_to_assertion_consumer_service_request(self):
        # Send request without SAML2Response parameter
        response = self.client.post(reverse('saml2_acs'))
        # Assert that view responded with "Bad Request" error
        self.assertEqual(response.status_code, 400)

    def test_bad_request_method_to_assertion_consumer_service(self):
        # Send request with non-POST method.
        response = self.client.get(reverse('saml2_acs'))
        # Assert that view responded with method not allowed status
        self.assertEqual(response.status_code, 405)

    def do_login(self):
        """Auxiliary method used in several tests (mainly logout tests)"""
        self.init_cookies()

        response = self.client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)
        # session_id should start with a letter since it is a NCName
        came_from = '/another-view/'
        self.add_outstanding_query(session_id, came_from)

        saml_response = auth_response(session_id, 'student')

        # this will create a user
        response = self.client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        subject_id = get_subject_id_from_saml2(saml_response)
        self.assertEqual(response.status_code, 302)
        return subject_id

    def test_echo_view_no_saml_session(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        self.do_login()

        request = RequestFactory().get('/bar/foo')
        request.COOKIES = self.client.cookies
        request.user = User.objects.last()

        middleware = SamlSessionMiddleware()
        middleware.process_request(request)

        response = EchoAttributesView.as_view()(request)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(
            response.content.decode(),
            'No active SAML identity found. Are you sure you have logged in via SAML?'
        )

    def test_echo_view_success(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        self.do_login()

        request = RequestFactory().get('/')
        request.user = User.objects.last()

        middleware = SamlSessionMiddleware()
        middleware.process_request(request)

        saml_session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME',
                                    'saml_session')
        getattr(
            request, saml_session_name
        )['_saml2_subject_id'] = '1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03'
        getattr(request, saml_session_name).save()

        response = EchoAttributesView.as_view()(request)
        self.assertEqual(response.status_code, 200)
        self.assertIn('<h1>SAML attributes</h1>', response.content.decode(),
                      'Echo page not rendered')

    def test_logout(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        self.do_login()

        response = self.client.get(reverse('saml2_logout'))
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path,
                         '/simplesaml/saml2/idp/SingleLogoutService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)

        saml_request = params['SAMLRequest'][0]

        self.assertIn('LogoutRequest xmlns',
                      decode_base64_and_inflate(saml_request).decode('utf-8'),
                      'Not a valid LogoutRequest')

    def test_logout_service_local(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        self.do_login()

        response = self.client.get(reverse('saml2_logout'))
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path,
                         '/simplesaml/saml2/idp/SingleLogoutService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)

        saml_request = params['SAMLRequest'][0]

        self.assertIn('LogoutRequest xmlns',
                      decode_base64_and_inflate(saml_request).decode('utf-8'),
                      'Not a valid LogoutRequest')

        # now simulate a logout response sent by the idp
        expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" Reason=""><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""

        request_id = re.findall(r' ID="(.*?)" ', expected_request)[0]
        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')

        saml_response = """<?xml version='1.0' encoding='UTF-8'?>
<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % (
            request_id, instant)

        response = self.client.get(
            reverse('saml2_ls'), {
                'SAMLResponse': deflate_and_base64_encode(saml_response),
            })
        self.assertContains(response, "Logged out", status_code=200)
        self.assertListEqual(list(self.client.session.keys()), [])

    def test_logout_service_global(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        subject_id = self.do_login()
        # now simulate a global logout process initiated by another SP
        subject_id = views._get_subject_id(self.saml_session)
        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
        saml_request = """<?xml version='1.0' encoding='UTF-8'?>
<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>""" % (
            instant, subject_id)

        response = self.client.get(
            reverse('saml2_ls'), {
                'SAMLRequest': deflate_and_base64_encode(saml_request),
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path,
                         '/simplesaml/saml2/idp/SingleLogoutService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLResponse', params)
        saml_response = params['SAMLResponse'][0]

        self.assertIn('Response xmlns',
                      decode_base64_and_inflate(saml_response).decode('utf-8'),
                      'Not a valid Response')

    def test_incomplete_logout(self):
        settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
                                                idp_hosts=['idp.example.com'])

        # don't do a login

        # now simulate a global logout process initiated by another SP
        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
        saml_request = '<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>' % (
            instant, 'invalid-subject-id')

        response = self.client.get(
            reverse('saml2_ls'), {
                'SAMLRequest': deflate_and_base64_encode(saml_request),
            })
        self.assertContains(response, 'Logout error', status_code=403)

    def test_finish_logout_renders_error_template(self):
        request = RequestFactory().get('/bar/foo')
        response = finish_logout(request, None)
        self.assertContains(response, "<h1>Logout error</h1>", status_code=200)

    def test_sigalg_not_passed_when_not_signing_request(self):
        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        with mock.patch(
                'djangosaml2.views.Saml2Client.prepare_for_authenticate',
                return_value=('session_id', {
                    'url': 'fake'
                }),
        ) as prepare_for_auth_mock:
            self.client.get(reverse('saml2_login'))
        prepare_for_auth_mock.assert_called_once()
        _args, kwargs = prepare_for_auth_mock.call_args
        self.assertNotIn('sigalg', kwargs)

    def test_sigalg_passed_when_signing_request(self):
        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        settings.SAML_CONFIG['service']['sp']['authn_requests_signed'] = True
        with mock.patch(
                'djangosaml2.views.Saml2Client.prepare_for_authenticate',
                return_value=('session_id', {
                    'url': 'fake'
                }),
        ) as prepare_for_auth_mock:
            self.client.get(reverse('saml2_login'))
        prepare_for_auth_mock.assert_called_once()
        _args, kwargs = prepare_for_auth_mock.call_args
        self.assertIn('sigalg', kwargs)
Beispiel #13
0
def spid_login(request,
          config_loader_path=None,
          wayf_template='wayf.html',
          authorization_error_template='djangosaml2/auth_error.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http POST protocol binding.
    """
    
    logger.debug('SPID Login process started')
    next_url = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not next_url:
        logger.warning('The next parameter exists but is empty')
        next_url = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not validate_referral_url(request, next_url):
        next_url = settings.LOGIN_REDIRECT_URL
    
    if request.user.is_authenticated:
        redirect_authenticated_user = getattr(settings,
                                              'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN',
                                              True)
        if redirect_authenticated_user:
            return HttpResponseRedirect(next_url)
        else: # pragma: no cover
            logger.debug('User is already logged in')
            return render(request, authorization_error_template, {
                    'came_from': next_url})
    
    # this works only if request came from wayf
    selected_idp = request.GET.get('idp', None)

    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render(request, wayf_template, 
            {
                'available_idps': idps.items(),
                'next_url': next_url
            }
        )
    else:
        # otherwise is the first one
        _msg = 'Unable to know which IdP to use'
        try:
            selected_idp = selected_idp or list(idps.keys())[0]
        except TypeError as e: # pragma: no cover
            logger.error(f'{_msg}: {e}')
            return HttpResponseError(_msg)
        except IndexError as e: # pragma: no cover
            logger.error(f'{_msg}: {e}')
            return HttpResponseNotFound(_msg)
    
    binding = settings.SPID_DEFAULT_BINDING
    logger.debug(f'Trying binding {binding} for IDP {selected_idp}')
    # ensure our selected binding is supported by the IDP
    supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf)
    if binding not in supported_bindings:
        _msg = (
                f"Requested: {binding} but the selected "
                f"IDP [{selected_idp}] doesn't support "
                f"{BINDING_HTTP_POST} or {BINDING_HTTP_REDIRECT}. "
                f"Check if IdP Metadata is correctly loaded and updated."
        )
        logger.error(_msg)
        raise UnsupportedBinding(_msg)

    # SPID things here
    try:
        login_response = spid_sp_authn_request(conf, 
                                               selected_idp, 
                                               binding, 
                                               settings.SPID_NAMEID_FORMAT,
                                               settings.SPID_AUTH_CONTEXT,
                                               settings.SPID_SIG_ALG,
                                               settings.SPID_DIG_ALG,
                                               next_url
        )
    except UnknownSystemEntity as e: # pragma: no cover
        _msg = f'Unknown IDP Entity ID: {selected_idp}'
        logger.error(f'{_msg}: {e}')
        return HttpResponseNotFound(_msg)
    
    session_id = login_response['session_id']
    http_response = login_response['http_response']
    
    # success, so save the session ID and return our response
    logger.debug(f'Saving session-id {session_id} in the OutstandingQueries cache')
    oq_cache = OutstandingQueriesCache(request.saml_session)
    oq_cache.set(session_id, next_url)
    
    if binding == saml2.BINDING_HTTP_POST:
        return HttpResponse(http_response['data'])
    elif binding == saml2.BINDING_HTTP_REDIRECT:
        headers = dict(login_response['http_response']['headers'])
        return HttpResponseRedirect(headers['Location'])
Beispiel #14
0
class SAML2Tests(TestCase):

    urls = 'djangosaml2.tests.urls'

    def setUp(self):
        if hasattr(settings, 'SAML_ATTRIBUTE_MAPPING'):
            self.actual_attribute_mapping = settings.SAML_ATTRIBUTE_MAPPING
            del settings.SAML_ATTRIBUTE_MAPPING
        if hasattr(settings, 'SAML_CONFIG_LOADER'):
            self.actual_conf_loader = settings.SAML_CONFIG_LOADER
            del settings.SAML_CONFIG_LOADER

    def tearDown(self):
        if hasattr(self, 'actual_attribute_mapping'):
            settings.SAML_ATTRIBUTE_MAPPING = self.actual_attribute_mapping
        if hasattr(self, 'actual_conf_loader'):
            settings.SAML_CONFIG_LOADER = self.actual_conf_loader

    def assertSAMLRequestsEquals(self, real_xml, expected_xmls):
        def remove_variable_attributes(xml_string):
            xml_string = re.sub(r' ID=".*?" ', ' ', xml_string)
            xml_string = re.sub(r' IssueInstant=".*?" ', ' ', xml_string)
            xml_string = re.sub(r'<saml:NameID(.*)>.*</saml:NameID>',
                                r'<saml:NameID\1></saml:NameID>', xml_string)

            return xml_string

        self.assertEqual(remove_variable_attributes(real_xml),
                         remove_variable_attributes(expected_xmls))

    def init_cookies(self):
        self.client.cookies[settings.SESSION_COOKIE_NAME] = 'testing'

    def add_outstanding_query(self, session_id, came_from):
        settings.SESSION_ENGINE = 'django.contrib.sessions.backends.db'
        engine = import_module(settings.SESSION_ENGINE)
        self.saml_session = engine.SessionStore()
        self.saml_session.save()
        self.oq_cache = OutstandingQueriesCache(self.saml_session)

        self.oq_cache.set(session_id \
                          if isinstance(session_id, str) else session_id.decode(),
                          came_from)
        self.saml_session.save()
        self.client.cookies[
            settings.SESSION_COOKIE_NAME] = self.saml_session.session_key

    def render_template(self, text):
        return Template(text).render(Context())

    def b64_for_post(self, xml_text, encoding='utf-8'):
        return base64.b64encode(xml_text.encode(encoding)).decode('ascii')

    def test_unsigned_post_authn_request(self):
        """
        Test that unsigned authentication requests via POST binding
        does not error.

        https://github.com/knaperek/djangosaml2/issues/168
        """
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_post_binding.xml',
            authn_requests_signed=False)
        response = self.client.get(reverse('saml2_login'))

        self.assertEqual(response.status_code, 200)

        # Using POST-binding returns a page with form containing the SAMLRequest
        response_parser = SAMLPostFormParser()
        response_parser.feed(response.content.decode('utf-8'))
        saml_request = response_parser.saml_request_value

        self.assertIsNotNone(saml_request)
        if 'AuthnRequest xmlns' not in base64.b64decode(saml_request).decode(
                'utf-8'):
            raise Exception(
                'test_unsigned_post_authn_request: Not a valid AuthnRequest')

    def test_login_evil_redirect(self):
        """
        Make sure that if we give an URL other than our own host as the next
        parameter, it is replaced with the default LOGIN_REDIRECT_URL.
        """

        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        response = self.client.get(
            reverse('saml2_login') + '?next=http://evil.com')
        url = urlparse(response['Location'])
        params = parse_qs(url.query)

        self.assertEqual(params['RelayState'], [
            settings.LOGIN_REDIRECT_URL,
        ])

    def test_login_one_idp(self):
        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        response = self.client.get(reverse('saml2_login'))
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)
        self.assertIn('RelayState', params)

        saml_request = params['SAMLRequest'][0]
        if 'AuthnRequest xmlns' not in decode_base64_and_inflate(
                saml_request).decode('utf-8'):
            raise Exception('Not a valid AuthnRequest')

        # if we set a next arg in the login view, it is preserverd
        # in the RelayState argument
        nexturl = '/another-view/'
        response = self.client.get(reverse('saml2_login'), {'next': nexturl})
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)
        self.assertIn('RelayState', params)
        self.assertEqual(params['RelayState'][0], nexturl)

    def test_login_several_idps(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=[
                'idp1.example.com', 'idp2.example.com', 'idp3.example.com'
            ],
            metadata_file='remote_metadata_three_idps.xml',
        )
        response = self.client.get(reverse('saml2_login'))
        # a WAYF page should be displayed
        self.assertContains(response, 'Where are you from?', status_code=200)
        for i in range(1, 4):
            link = '/login/?idp=https://idp%d.example.com/simplesaml/saml2/idp/metadata.php&next=/'
            self.assertContains(response, link % i)

        # click on the second idp
        response = self.client.get(
            reverse('saml2_login'), {
                'idp':
                'https://idp2.example.com/simplesaml/saml2/idp/metadata.php',
                'next': '/',
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp2.example.com')
        self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)
        self.assertIn('RelayState', params)

        saml_request = params['SAMLRequest'][0]
        if 'AuthnRequest xmlns' not in decode_base64_and_inflate(
                saml_request).decode('utf-8'):
            raise Exception('Not a valid AuthnRequest')

    def test_assertion_consumer_service(self):
        # Get initial number of users
        initial_user_count = User.objects.count()
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        response = self.client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)
        # session_id should start with a letter since it is a NCName
        came_from = '/another-view/'
        self.add_outstanding_query(session_id, came_from)

        # this will create a user
        saml_response = auth_response(session_id, 'student')
        _url = reverse('saml2_acs')
        response = self.client.post(
            _url, {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']
        url = urlparse(location)
        self.assertEqual(url.path, came_from)

        self.assertEqual(User.objects.count(), initial_user_count + 1)
        user_id = self.client.session[SESSION_KEY]
        user = User.objects.get(id=user_id)
        self.assertEqual(user.username, 'student')

        # let's create another user and log in with that one
        new_user = User.objects.create(username='******', password='******')

        #  session_id = "a1111111111111111111111111111111"
        client = Client()
        response = client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)

        came_from = ''  # bad, let's see if we can deal with this
        saml_response = auth_response(session_id, 'teacher')
        self.add_outstanding_query(session_id, '/')
        response = client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        # as the RelayState is empty we have redirect to LOGIN_REDIRECT_URL
        self.assertEqual(url.path, settings.LOGIN_REDIRECT_URL)
        self.assertEqual(force_text(new_user.id), client.session[SESSION_KEY])

    def test_assertion_consumer_service_no_session(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        response = self.client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)
        # session_id should start with a letter since it is a NCName

        came_from = '/another-view/'
        self.add_outstanding_query(session_id, came_from)

        # Authentication is confirmed.
        saml_response = auth_response(session_id, 'student')
        response = self.client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']
        url = urlparse(location)
        self.assertEqual(url.path, came_from)

        # Session should no longer be in outstanding queries.
        saml_response = auth_response(session_id, 'student')
        response = self.client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        self.assertEqual(response.status_code, 403)

    def test_missing_param_to_assertion_consumer_service_request(self):
        # Send request without SAML2Response parameter
        response = self.client.post(reverse('saml2_acs'))
        # Assert that view responded with "Bad Request" error
        self.assertEqual(response.status_code, 400)

    def test_bad_request_method_to_assertion_consumer_service(self):
        # Send request with non-POST method.
        response = self.client.get(reverse('saml2_acs'))
        # Assert that view responded with method not allowed status
        self.assertEqual(response.status_code, 405)

    def do_login(self):
        """Auxiliary method used in several tests (mainly logout tests)"""
        self.init_cookies()

        response = self.client.get(reverse('saml2_login'))
        saml2_req = saml2_from_httpredirect_request(response.url)
        session_id = get_session_id_from_saml2(saml2_req)
        # session_id should start with a letter since it is a NCName
        came_from = '/another-view/'
        self.add_outstanding_query(session_id, came_from)

        saml_response = auth_response(session_id, 'student')

        # this will create a user
        response = self.client.post(
            reverse('saml2_acs'), {
                'SAMLResponse': self.b64_for_post(saml_response),
                'RelayState': came_from,
            })
        subject_id = get_subject_id_from_saml2(saml_response)
        self.assertEqual(response.status_code, 302)
        return subject_id

    @skip(
        "This is a known issue caused by pysaml2. Needs more investigation. Fixes are welcome."
    )
    def test_logout(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        self.do_login()

        response = self.client.get(reverse('saml2_logout'))
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path,
                         '/simplesaml/saml2/idp/SingleLogoutService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)

        saml_request = params['SAMLRequest'][0]

        if 'LogoutRequest xmlns' not in decode_base64_and_inflate(
                saml_request).decode('utf-8'):
            raise Exception('Not a valid LogoutRequest')

    def test_logout_service_local(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        self.do_login()

        response = self.client.get(reverse('saml2_logout'))
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path,
                         '/simplesaml/saml2/idp/SingleLogoutService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLRequest', params)

        saml_request = params['SAMLRequest'][0]
        if 'LogoutRequest xmlns' not in decode_base64_and_inflate(
                saml_request).decode('utf-8'):
            raise Exception('Not a valid LogoutRequest')

        # now simulate a logout response sent by the idp
        expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" Reason=""><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""

        request_id = re.findall(r' ID="(.*?)" ', expected_request)[0]
        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')

        saml_response = """<?xml version='1.0' encoding='UTF-8'?>
<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="http://sp.example.com/saml2/ls/" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="%s" IssueInstant="%s" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>""" % (
            request_id, instant)

        response = self.client.get(
            reverse('saml2_ls'), {
                'SAMLResponse': deflate_and_base64_encode(saml_response),
            })
        self.assertContains(response, "Logged out", status_code=200)
        self.assertListEqual(list(self.client.session.keys()), [])

    def test_logout_service_global(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        subject_id = self.do_login()
        # now simulate a global logout process initiated by another SP
        subject_id = views._get_subject_id(self.saml_session)
        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
        saml_request = """<?xml version='1.0' encoding='UTF-8'?>
<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>""" % (
            instant, subject_id)

        response = self.client.get(
            reverse('saml2_ls'), {
                'SAMLRequest': deflate_and_base64_encode(saml_request),
            })
        self.assertEqual(response.status_code, 302)
        location = response['Location']

        url = urlparse(location)
        self.assertEqual(url.hostname, 'idp.example.com')
        self.assertEqual(url.path,
                         '/simplesaml/saml2/idp/SingleLogoutService.php')

        params = parse_qs(url.query)
        self.assertIn('SAMLResponse', params)
        saml_response = params['SAMLResponse'][0]

        if 'Response xmlns' not in decode_base64_and_inflate(
                saml_response).decode('utf-8'):
            raise Exception('Not a valid Response')

    def test_incomplete_logout(self):
        settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
                                                idp_hosts=['idp.example.com'])

        # don't do a login

        # now simulate a global logout process initiated by another SP
        instant = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
        saml_request = '<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_9961abbaae6d06d251226cb25e38bf8f468036e57e" Version="2.0" IssueInstant="%s" Destination="http://sp.example.com/saml2/ls/"><saml:Issuer>https://idp.example.com/simplesaml/saml2/idp/metadata.php</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">%s</saml:NameID><samlp:SessionIndex>_1837687b7bc9faad85839dbeb319627889f3021757</samlp:SessionIndex></samlp:LogoutRequest>' % (
            instant, 'invalid-subject-id')

        response = self.client.get(
            reverse('saml2_ls'), {
                'SAMLRequest': deflate_and_base64_encode(saml_request),
            })
        self.assertContains(response, 'Logout error', status_code=403)

    def test_finish_logout_renders_error_template(self):
        request = RequestFactory().get('/bar/foo')
        response = finish_logout(request, None)
        self.assertContains(response, "<h1>Logout error</h1>", status_code=200)

    def _test_metadata(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )
        valid_until = datetime.datetime.utcnow() + datetime.timedelta(hours=24)
        valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
        expected_metadata = """<?xml version='1.0' encoding='UTF-8'?>
<md:EntityDescriptor entityID="http://sp.example.com/saml2/metadata/" validUntil="%s" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"><md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>MIIDPjCCAiYCCQCkHjPQlll+mzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJF
UzEQMA4GA1UECBMHU2V2aWxsYTEbMBkGA1UEChMSWWFjbyBTaXN0ZW1hcyBTLkwu
MRAwDgYDVQQHEwdTZXZpbGxhMREwDwYDVQQDEwh0aWNvdGljbzAeFw0wOTEyMDQx
OTQzNTJaFw0xMDEyMDQxOTQzNTJaMGExCzAJBgNVBAYTAkVTMRAwDgYDVQQIEwdT
ZXZpbGxhMRswGQYDVQQKExJZYWNvIFNpc3RlbWFzIFMuTC4xEDAOBgNVBAcTB1Nl
dmlsbGExETAPBgNVBAMTCHRpY290aWNvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLEfkxo
Pk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+tjy1b
YV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB+gY1
77DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDUOQLx
4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zuWxYd
T9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABMA0GCSqGSIb3DQEBBQUAA4IB
AQCQBhKOqucJZAqGHx4ybDXNzpPethszonLNVg5deISSpWagy55KlGCi5laio/xq
hHRx18eTzeCeLHQYvTQxw0IjZOezJ1X30DD9lEqPr6C+IrmZc6bn/pF76xsvdaRS
gduNQPT1B25SV2HrEmbf8wafSlRARmBsyUHh860TqX7yFVjhYIAUF/El9rLca51j
ljCIqqvT+klPdjQoZwODWPFHgute2oNRmoIcMjSnoy1+mxOC2Q/j7kcD8/etulg2
XDxB3zD81gfdtT8VBFP+G4UrBa+5zFk6fT6U8a7ZqVsyH+rCXAdCyVlEC4Y5fZri
ID4zT0FcZASGuthM56rRJJSx
</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://sp.example.com/saml2/ls/" /><md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://sp.example.com/saml2/acs/" index="1" /><md:AttributeConsumingService index="1"><md:ServiceName xml:lang="en">Test SP</md:ServiceName><md:RequestedAttribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><md:RequestedAttribute FriendlyName="eduPersonAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false" /></md:AttributeConsumingService></md:SPSSODescriptor><md:Organization><md:OrganizationName xml:lang="es">Ejemplo S.A.</md:OrganizationName><md:OrganizationName xml:lang="en">Example Inc.</md:OrganizationName><md:OrganizationDisplayName xml:lang="es">Ejemplo</md:OrganizationDisplayName><md:OrganizationDisplayName xml:lang="en">Example</md:OrganizationDisplayName><md:OrganizationURL xml:lang="es">http://www.example.es</md:OrganizationURL><md:OrganizationURL xml:lang="en">http://www.example.com</md:OrganizationURL></md:Organization><md:ContactPerson contactType="technical"><md:Company>Example Inc.</md:Company><md:GivenName>Technical givenname</md:GivenName><md:SurName>Technical surname</md:SurName><md:EmailAddress>[email protected]</md:EmailAddress></md:ContactPerson><md:ContactPerson contactType="administrative"><md:Company>Example Inc.</md:Company><md:GivenName>Administrative givenname</md:GivenName><md:SurName>Administrative surname</md:SurName><md:EmailAddress>[email protected]</md:EmailAddress></md:ContactPerson></md:EntityDescriptor>"""

        expected_metadata = expected_metadata % valid_until

        response = self.client.get('/metadata/')
        self.assertEqual(response['Content-type'], 'text/xml; charset=utf8')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content, expected_metadata)

    def test_post_authenticated_signal(self):
        self.called = []

        def signal_handler(sender, instance, session_info, request, **kwargs):
            self.called.append({
                'sender': sender,
                'instance': instance,
                'request': request,
                'session_info': session_info
            })

        post_authenticated.connect(signal_handler, dispatch_uid='test_signal')

        self.do_login()

        # make sure the handler is only called once
        self.assertEqual(len(self.called), 1)
        # test 'sender', this should be User.__class__
        self.assertEqual(self.called[0]['sender'], get_user_model(),
                         'post_authenticated signal sender is not a User')
        # test 'instance', this should be User
        self.assertIsInstance(
            self.called[0]['instance'], get_user_model(),
            'post_authenticated signal did not send a User instance')
        # test the request
        self.assertIsInstance(
            self.called[0]['request'], HttpRequest,
            'post_authenticated signal did not send a request')
        # test the session_info
        self.assertIsInstance(
            self.called[0]['session_info'], dict,
            'post_authenticated signal did not send a session_info dict')

        post_authenticated.disconnect(dispatch_uid='test_signal')

    def test_pre_user_save_signal(self):
        self.called = []

        def signal_handler(sender, instance, attributes, user_modified,
                           **kwargs):
            self.called.append({
                'sender': sender,
                'instance': instance,
                'attributes': attributes,
                'user_modified': user_modified
            })

        pre_user_save.connect(signal_handler, dispatch_uid='test_signal')

        self.do_login()

        # make sure the handler is only called once
        self.assertEqual(len(self.called), 1)
        # test 'sender', this should be User.__class__
        self.assertEqual(self.called[0]['sender'], get_user_model(),
                         'pre_user_save signal sender is not a User')
        # test 'instance', this should be User
        self.assertIsInstance(
            self.called[0]['instance'], get_user_model(),
            'pre_user_save signal did not send a User instance')
        # test the attributes
        self.assertIsInstance(self.called[0]['attributes'], dict,
                              'pre_user_save signal did not send attributes')
        # test the user_modified
        self.assertIsInstance(
            self.called[0]['user_modified'], bool,
            'pre_user_save signal did not send a user_modified bool')

        pre_user_save.disconnect(dispatch_uid='test_signal')

    def test_idplist_templatetag(self):
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=[
                'idp1.example.com', 'idp2.example.com', 'idp3.example.com'
            ],
            metadata_file='remote_metadata_three_idps.xml',
        )
        rendered = self.render_template('{% load idplist %}'
                                        '{% idplist as idps %}'
                                        '{% for url, name in idps.items %}'
                                        '{{ url }} - {{ name }}; '
                                        '{% endfor %}')

        # the idplist is unordered, so convert the result into a set.
        rendered = set(rendered.split('; '))
        expected = set([
            u'https://idp1.example.com/simplesaml/saml2/idp/metadata.php - idp1.example.com IdP',
            u'https://idp2.example.com/simplesaml/saml2/idp/metadata.php - idp2.example.com IdP',
            u'https://idp3.example.com/simplesaml/saml2/idp/metadata.php - idp3.example.com IdP',
            u'',
        ])

        self.assertEqual(rendered, expected)

    def test_sigalg_not_passed_when_not_signing_request(self):
        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        with mock.patch(
                'djangosaml2.views.Saml2Client.prepare_for_authenticate',
                return_value=('session_id', {
                    'url': 'fake'
                }),
        ) as prepare_for_auth_mock:
            self.client.get(reverse('saml2_login'))
        prepare_for_auth_mock.assert_called_once()
        _args, kwargs = prepare_for_auth_mock.call_args
        self.assertNotIn('sigalg', kwargs)

    def test_sigalg_passed_when_signing_request(self):
        # monkey patch SAML configuration
        settings.SAML_CONFIG = conf.create_conf(
            sp_host='sp.example.com',
            idp_hosts=['idp.example.com'],
            metadata_file='remote_metadata_one_idp.xml',
        )

        settings.SAML_CONFIG['service']['sp']['authn_requests_signed'] = True
        with mock.patch(
                'djangosaml2.views.Saml2Client.prepare_for_authenticate',
                return_value=('session_id', {
                    'url': 'fake'
                }),
        ) as prepare_for_auth_mock:
            self.client.get(reverse('saml2_login'))
        prepare_for_auth_mock.assert_called_once()
        _args, kwargs = prepare_for_auth_mock.call_args
        self.assertIn('sigalg', kwargs)
Beispiel #15
0
    location = result[1]

    # fix up the redirect url for endpoints that have ? in the link
    split_location = location.split("?SAMLRequest=")
    if split_location and "?" in split_location[0]:
        logger.debug("Redirect URL already has query string, " + "transforming ?SAMLRequest=")
        location = location.replace("?SAMLRequest=", "&SAMLRequest=")

    split_location = location.split("?RelayState=")
    if split_location and "?" in split_location[0]:
        logger.debug("Redirect URL already has query string, " + "transforming ?RelayState=")
        location = location.replace("?RelayState=", "&RelayState=")

    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 the user to the IdP")
    logger.debug("Redirecting to %s" % location)
    return HttpResponseRedirect(location)


@require_POST
@csrf_exempt
def assertion_consumer_service(request, config_loader_path=None, attribute_mapping=None, create_unknown_user=None):
    """SAML Authorization Response endpoint

    The IdP will send its response to this view, which
    will process it with pysaml2 help and log the user
    in using the custom Authorization backend
    djangosaml2.backends.Saml2Backend that should be
Beispiel #16
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html',
          post_binding_form_template='djangosaml2/post_binding_form.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.

    * post_binding_form_template - path to a template containing HTML form with
    hidden input elements, used to send the SAML message data when HTTP POST
    binding is being used. You can customize this template to include custom
    branding and/or text explaining the automatic redirection process. Please
    see the example template in
    templates/djangosaml2/example_post_binding_form.html
    If set to None or nonexistent template, default form from the saml2 library
    will be rendered.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # Ensure the user-originating redirection url is safe.
    if not is_safe_url_compat(url=came_from, allowed_hosts={request.get_host()}):
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if callable_bool(request.user.is_authenticated):
        redirect_authenticated_user = getattr(settings, 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN', True)
        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render(request, authorization_error_template, {
                    'came_from': came_from,
                    })

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render(request, wayf_template, {
                'available_idps': idps.items(),
                'came_from': came_from,
                })

    # choose a binding to try first
    sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
    binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT
    logger.debug('Trying binding %s for IDP %s', binding, selected_idp)

    # ensure our selected binding is supported by the IDP
    supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf)
    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
        else:
            logger.warning('IDP %s does not support %s,  trying %s',
                           selected_idp, binding, BINDING_HTTP_POST)
            binding = BINDING_HTTP_POST
        # if switched binding still not supported, give up
        if binding not in supported_bindings:
            raise UnsupportedBinding('IDP %s does not support %s or %s',
                                     selected_idp, BINDING_HTTP_POST, BINDING_HTTP_REDIRECT)

    client = Saml2Client(conf)
    http_response = None

    logger.debug('Redirecting user to the IdP via %s binding.', binding)
    if binding == BINDING_HTTP_REDIRECT:
        try:
            # do not sign the xml itself, instead use the sigalg to
            # generate the signature as a URL param
            sig_alg_option_map = {'sha1': SIG_RSA_SHA1,
                                  'sha256': SIG_RSA_SHA256}
            sig_alg_option = getattr(conf, '_sp_authn_requests_signed_alg', 'sha1')
            sigalg = sig_alg_option_map[sig_alg_option] if sign_requests else None
            nsprefix = get_namespace_prefixes()
            session_id, result = client.prepare_for_authenticate(
                entityid=selected_idp, relay_state=came_from,
                binding=binding, sign=False, sigalg=sigalg,
                nsprefix=nsprefix)
        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:
        if post_binding_form_template:
            # get request XML to build our own html based on the template
            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)
            try:
                if PY3:
                    saml_request = base64.b64encode(binary_type(request_xml, 'UTF-8'))
                else:
                    saml_request = base64.b64encode(binary_type(request_xml))

                http_response = render(request, post_binding_form_template, {
                    'target_url': location,
                    'params': {
                        'SAMLRequest': saml_request,
                        'RelayState': came_from,
                        },
                    })
            except TemplateDoesNotExist:
                pass

        if not http_response:
            # use the html provided by pysaml2 if no template was specified or it didn't exist
            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'])
    else:
        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
Beispiel #17
0
 def add_outstanding_query(self, session_id, came_from):
     session = self.client.session
     oq_cache = OutstandingQueriesCache(session)
     oq_cache.set(session_id, came_from)
     session.save()
     self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key
Beispiel #18
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if not request.user.is_anonymous():
        try:
            redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
        except AttributeError:
            redirect_authenticated_user = True

        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render_to_response(authorization_error_template, {
                    'came_from': came_from,
                    }, context_instance=RequestContext(request))

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)

    # is a embedded wayf needed?
    idps = available_idps(conf)
    if selected_idp is None and len(idps) > 1:
        logger.debug('A discovery process is needed')
        return render_to_response(wayf_template, {
                'available_idps': idps.items(),
                'came_from': came_from,
                }, context_instance=RequestContext(request))

    client = Saml2Client(conf)
    try:
        (session_id, result) = client.prepare_for_authenticate(
            entityid=selected_idp, relay_state=came_from,
            binding=BINDING_HTTP_REDIRECT,
            )
    except TypeError as e:
        logger.error('Unable to know which IdP to use')
        return HttpResponse(unicode(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 the user to the IdP')
    return HttpResponseRedirect(get_location(result))
Beispiel #19
0
    def post(self, request):
        if not self.request.user.is_anonymous:
            error_message = _('This endpoint is for anonymous users only.')
            return JsonResponse({'error_message': error_message}, status_code=400)

        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        idp = serializer.validated_data.get('idp')

        conf = get_config(request=request)

        # ensure our selected binding is supported by the IDP
        supported_bindings = utils.get_idp_sso_supported_bindings(idp, config=conf)
        default_binding = settings.WALDUR_AUTH_SAML2.get('DEFAULT_BINDING')

        if default_binding in supported_bindings:
            binding = default_binding
        elif BINDING_HTTP_POST in supported_bindings:
            binding = BINDING_HTTP_POST
        elif BINDING_HTTP_REDIRECT in supported_bindings:
            binding = BINDING_HTTP_REDIRECT
        else:
            error_message = _('Identity provider does not support available bindings.')
            return JsonResponse({'error_message': error_message}, status_code=400)

        client = Saml2Client(conf)

        kwargs = {}
        sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
        if sign_requests:
            signature_algorithm = settings.WALDUR_AUTH_SAML2.get('signature_algorithm') or SIG_RSA_SHA1
            digest_algorithm = settings.WALDUR_AUTH_SAML2.get('digest_algorithm') or DIGEST_SHA1

            kwargs['sign'] = True
            kwargs['sigalg'] = signature_algorithm
            kwargs['sign_alg'] = signature_algorithm
            kwargs['digest_alg'] = digest_algorithm

        nameid_format = settings.WALDUR_AUTH_SAML2.get('nameid_format')
        if nameid_format or nameid_format == "":  # "" is a valid setting in pysaml2
            kwargs['nameid_format'] = nameid_format

        if binding == BINDING_HTTP_REDIRECT:
            session_id, result = client.prepare_for_authenticate(
                entityid=idp, binding=binding, **kwargs)

            data = {
                'binding': 'redirect',
                'url': get_location(result),
            }
        elif binding == BINDING_HTTP_POST:
            try:
                location = client.sso_location(idp, binding)
            except TypeError:
                error_message = _('Invalid identity provider specified.')
                return JsonResponse({'error_message': error_message}, status_code=400)

            session_id, request_xml = client.create_authn_request(location, binding=binding, **kwargs)
            data = {
                'binding': 'post',
                'url': location,
                'request': base64.b64encode(request_xml.encode('UTF-8')),
            }

        # save session_id
        oq_cache = OutstandingQueriesCache(request.session)
        oq_cache.set(session_id, '')

        return JsonResponse(data)
Beispiel #20
0
def login(request,
          config_loader_path=None,
          wayf_template='djangosaml2/wayf.html',
          authorization_error_template='djangosaml2/auth_error.html'):
    """SAML Authorization Request initiator

    This view initiates the SAML2 Authorization handshake
    using the pysaml2 library to create the AuthnRequest.
    It uses the SAML 2.0 Http Redirect protocol binding.
    """
    logger.debug('Login process started')

    came_from = request.GET.get('next', settings.LOGIN_REDIRECT_URL)
    if not came_from:
        logger.warning('The next parameter exists but is empty')
        came_from = settings.LOGIN_REDIRECT_URL

    # if the user is already authenticated that maybe because of two reasons:
    # A) He has this URL in two browser windows and in the other one he
    #    has already initiated the authenticated session.
    # B) He comes from a view that (incorrectly) send him here because
    #    he does not have enough permissions. That view should have shown
    #    an authorization error in the first place.
    # We can only make one thing here and that is configurable with the
    # SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
    # is True (default value) we will redirect him to the came_from view.
    # Otherwise, we will show an (configurable) authorization error.
    if not request.user.is_anonymous():
        try:
            redirect_authenticated_user = settings.SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN
        except AttributeError:
            redirect_authenticated_user = True

        if redirect_authenticated_user:
            return HttpResponseRedirect(came_from)
        else:
            logger.debug('User is already logged in')
            return render_to_response(authorization_error_template, {
                'came_from': came_from,
                }, context_instance=RequestContext(request))

    selected_idp = request.GET.get('idp', None)
    conf = get_config(config_loader_path, request)
    client = Saml2Client(conf)
    try:
        sid, http_args = client.prepare_for_authenticate(
            entityid=selected_idp, relay_state=came_from,
            binding=BINDING_HTTP_REDIRECT,
            )
    except TypeError as e:
        logger.error('Unable to know which IdP to use')
        raise e

    assert isinstance(sid, str)
    assert len(http_args) == 4
    assert http_args["headers"][0][0] == "Location"
    assert http_args["data"] == []
    location = http_args["headers"][0][1]

    # fix up the redirect url for endpoints that have ? in the link
    split_location = location.split('?SAMLRequest=')
    if split_location and '?' in split_location[0]:
        logger.debug(
            'Redirect URL already has query string, ' +
            'transforming ?SAMLRequest=')
        location = location.replace('?SAMLRequest=', '&SAMLRequest=')

    split_location = location.split('?RelayState=')
    if split_location and '?' in split_location[0]:
        logger.debug(
            'Redirect URL already has query string, ' +
            'transforming ?RelayState=')
        location = location.replace('?RelayState=', '&RelayState=')

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

    logger.debug('Redirecting the user to the IdP')
    logger.debug('Redirecting to %s' % location)
    return HttpResponseRedirect(location)