def logout(request, config_loader_path=None):
    """SAML Logout Request initiator

    This view initiates the SAML2 Logout request
    using the pysaml2 library to create the LogoutRequest.
    """
    state = StateCache(request.session)
    conf = get_config(config_loader_path, request)

    client = Saml2Client(conf,
                         state_cache=state,
                         identity_cache=IdentityCache(request.session))
    subject_id = _get_subject_id(request.session)
    if subject_id is None:
        logger.warning(
            'The session does not contain the subject id for user %s',
            request.user)

    sign_requests = getattr(conf, '_sp_logout_requests_signed', False)
    sign_alg = getattr(conf, 'signing_algorithm', SIG_RSA_SHA1)
    digest_alg = getattr(conf, 'digest_algorithm', DIGEST_SHA1)
    result = client.global_logout(subject_id,
                                  sign=sign_requests,
                                  sign_alg=sign_alg,
                                  digest_alg=digest_alg)

    state.sync()

    if not result:
        logger.error("Looks like the user %s is not logged in any IdP/AA",
                     subject_id)
        return HttpResponseBadRequest("You are not logged in any IdP/AA")

    if len(result) > 1:
        logger.error(
            'Sorry, I do not know how to logout from several sources. I will logout just from the first one'
        )

    for entityid, logout_info in result.items():
        if isinstance(logout_info, tuple):
            binding, http_info = logout_info
            if binding == BINDING_HTTP_POST:
                logger.debug(
                    'Returning form to the IdP to continue the logout process')
                body = ''.join(http_info['data'])
                return HttpResponse(body)
            elif binding == BINDING_HTTP_REDIRECT:
                logger.debug(
                    'Redirecting to the IdP to continue the logout process')
                return HttpResponseRedirect(get_location(http_info))
            else:
                logger.error('Unknown binding: %s', binding)
                return HttpResponseServerError('Failed to log out')
        else:
            # We must have had a soap logout
            return finish_logout(request, logout_info)

    logger.error(
        'Could not logout because there only the HTTP_REDIRECT is supported')
    return HttpResponseServerError('Logout Binding not supported')
Exemple #2
0
def do_logout_service(request,
                      data,
                      binding,
                      config_loader_path=None,
                      next_page=None,
                      logout_error_template='djangosaml2/logout_error.html'):
    """SAML Logout Response endpoint

    The IdP will send the logout response to this view,
    which will process it with pysaml2 help and log the user
    out.
    Note that the IdP can request a logout even when
    we didn't initiate the process as a single logout
    request started by another SP.
    """
    logger.debug('Logout service started')
    conf = get_config(config_loader_path, request)

    state = StateCache(request.session)
    client = Saml2Client(conf,
                         state_cache=state,
                         identity_cache=IdentityCache(request.session))

    if 'SAMLResponse' in data:  # we started the logout
        logger.debug('Receiving a logout response from the IdP')
        response = client.parse_logout_request_response(
            data['SAMLResponse'], binding)
        state.sync()
        return finish_logout(request, response, next_page=next_page)

    elif 'SAMLRequest' in data:  # logout started by the IdP
        logger.debug('Receiving a logout request from the IdP')
        subject_id = _get_subject_id(request.session)
        if subject_id is None:
            logger.warning(
                'The session does not contain the subject id for user %s. Performing local logout',
                request.user)
            auth.logout(request)
            return render(request, logout_error_template, status=403)
        else:
            http_info = client.handle_logout_request(data['SAMLRequest'],
                                                     subject_id,
                                                     binding,
                                                     relay_state=data.get(
                                                         'RelayState', ''))
            state.sync()
            auth.logout(request)
            if (http_info.get('method', 'GET') == 'POST'
                    and 'data' in http_info and
                ('Content-type', 'text/html') in http_info.get('headers', [])):
                # need to send back to the IDP a signed POST response with user session
                # return HTML form content to browser with auto form validation
                # to finally send request to the IDP
                return HttpResponse(http_info['data'])
            else:
                return HttpResponseRedirect(get_location(http_info))
    else:
        logger.error('No SAMLResponse or SAMLRequest parameter found')
        raise Http404('No SAMLResponse or SAMLRequest parameter found')
Exemple #3
0
def logout(request, config_loader_path=None):
    """SAML Logout Request initiator

    This view initiates the SAML2 Logout request
    using the pysaml2 library to create the LogoutRequest.
    """
    initiate_global_logout = get_custom_setting('DJANGOSAML_INITIATE_GLOBAL_LOGOUT', default=True)
    # Handle SP that should NOT initiate a global logout
    if not initiate_global_logout:
        logger.debug('SP cannot initiate a global logout. Doing local logout for %s and redirecting to %s' % (request.user, settings.LOGOUT_REDIRECT_URL))
        auth.logout(request)
        return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL)    
    state = StateCache(request.session)
    conf = get_config(config_loader_path, request)

    client = Saml2Client(conf, state_cache=state,
                         identity_cache=IdentityCache(request.session))
    subject_id = _get_subject_id(request.session)
    if subject_id is None:
        logger.warning(
            'The session does not contain the subject id for user %s',
            request.user)

    result = client.global_logout(subject_id)

    state.sync()

    if not result:
        logger.error("Looks like the user %s is not logged in any IdP/AA", subject_id)
        return HttpResponseBadRequest("You are not logged in any IdP/AA")

    if len(result) > 1:
        logger.error('Sorry, I do not know how to logout from several sources. I will logout just from the first one')

    for entityid, logout_info in result.items():
        if isinstance(logout_info, tuple):
            binding, http_info = logout_info
            if binding == BINDING_HTTP_POST:
                logger.debug('Returning form to the IdP to continue the logout process')
                body = ''.join(http_info['data'])
                return HttpResponse(body)
            elif binding == BINDING_HTTP_REDIRECT:
                logger.debug('Redirecting to the IdP to continue the logout process')
                return HttpResponseRedirect(get_location(http_info))
            else:
                logger.error('Unknown binding: %s', binding)
                return HttpResponseServerError('Failed to log out')
        else:
            # We must have had a soap logout
            return finish_logout(request, logout_info)

    logger.error('Could not logout because there only the HTTP_REDIRECT is supported')
    return HttpResponseServerError('Logout Binding not supported')
Exemple #4
0
def echo_attributes(request,
                    config_loader_path=None,
                    template='djangosaml2/echo_attributes.html'):
    """Example view that echo the SAML attributes of an user"""
    state = StateCache(request.session)
    conf = get_config(config_loader_path, request)

    client = Saml2Client(conf, state_cache=state,
                         identity_cache=IdentityCache(request.session))
    subject_id = _get_subject_id(request.session)
    try:
        identity = client.users.get_identity(subject_id,
                                             check_not_on_or_after=False)
    except AttributeError:
        return HttpResponse("No active SAML identity found. Are you sure you have logged in via SAML?")

    return render(request, template, {'attributes': identity[0]})
Exemple #5
0
def spid_logout(request, config_loader_path=None, **kwargs):
    """SAML Logout Request initiator

    This view initiates the SAML2 Logout request
    using the pysaml2 library to create the LogoutRequest.
    """
    if not request.user.is_authenticated:
        return HttpResponseRedirect(settings.LOGOUT_REDIRECT_URL)

    state = StateCache(request.saml_session)
    conf = get_config(config_loader_path, request)
    client = Saml2Client(conf,
                         state_cache=state,
                         identity_cache=IdentityCache(request.saml_session))

    # whatever happens, however, the user will be logged out of this sp
    auth.logout(request)
    state.sync()

    subject_id = djangosaml2_views._get_subject_id(request.saml_session)
    if subject_id is None:
        logger.warning(
            f"The session does not contain the subject id for user {request.user}"
        )
        logger.error(
            f"Looks like the user {subject_id} is not logged in any IdP/AA")
        return HttpResponseBadRequest("You are not logged in any IdP/AA")

    slo_req = saml2.samlp.LogoutRequest()

    slo_req.destination = subject_id.name_qualifier
    # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...)
    slo_req.attribute_consuming_service_index = "0"

    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"
    slo_req.issuer = issuer

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

    # oggetto
    slo_req.name_id = subject_id

    try:
        session_info = client.users.get_info_from(slo_req.name_id,
                                                  subject_id.name_qualifier,
                                                  False)
    except KeyError as e:
        logger.error(f"SPID Logout error: {e}")
        return HttpResponseRedirect("/")

    session_indexes = [session_info["session_index"]]

    # aggiungere session index
    if session_indexes:
        sis = []
        for si in session_indexes:
            if isinstance(si, saml2.samlp.SessionIndex):
                sis.append(si)
            else:
                sis.append(saml2.samlp.SessionIndex(text=si))
        slo_req.session_index = sis

    slo_req.protocol_binding = SAML2_DEFAULT_BINDING

    assertion_consumer_service_url = client.config._sp_endpoints[
        "assertion_consumer_service"][0][0]
    slo_req.assertion_consumer_service_url = assertion_consumer_service_url

    slo_req_signed = client.sign(
        slo_req,
        sign_prepare=False,
        sign_alg=settings.SPID_SIG_ALG,
        digest_alg=settings.SPID_DIG_ALG,
    )

    _req_str = slo_req_signed
    logger.debug(
        f"LogoutRequest to {subject_id.name_qualifier}: {repr_saml_request(_req_str)}"
    )

    slo_location = client.metadata.single_logout_service(
        subject_id.name_qualifier, SAML2_DEFAULT_BINDING,
        "idpsso")[0]["location"]

    if not slo_location:
        error_message = f"Unable to know SLO endpoint in {subject_id.name_qualifier}"
        logger.error(error_message)
        return HttpResponse(error_message)

    http_info = client.apply_binding(
        SAML2_DEFAULT_BINDING,
        _req_str,
        slo_location,
        sign=True,
        sigalg=settings.SPID_SIG_ALG,
    )
    state.sync()
    return HttpResponse(http_info["data"])
Exemple #6
0
def spid_sp_authn_request(conf, selected_idp, next_url=""):
    client = Saml2Client(conf)

    logger.debug(f"Redirecting user to the IdP via {SAML2_DEFAULT_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, SAML2_DEFAULT_BINDING)

    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"

    # issuer
    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()
    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

    # if SPID authentication level is > 1 then forceauthn must be True
    authn_req.force_authn = settings.SPID_ACR_FAUTHN_MAP[settings.SPID_AUTH_CONTEXT]

    authn_req.protocol_binding = SAML2_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

    authn_req_signed = client.sign(
        authn_req,
        sign_prepare=False,
        sign_alg=settings.SPID_SIG_ALG,
        digest_alg=settings.SPID_DIG_ALG,
    )

    logger.debug(f"AuthRequest to {selected_idp}: {authn_req_signed}")

    relay_state = next_url or reverse("djangosaml2:saml2_echo_attributes")
    http_info = client.apply_binding(
        SAML2_DEFAULT_BINDING,
        authn_req_signed,
        location,
        sign=True,
        sigalg=settings.SPID_SIG_ALG,
        relay_state=relay_state,
    )

    return dict(
        http_response=http_info,
        authn_request=authn_req_signed,
        relay_state=relay_state,
        session_id=authn_req.id,
    )
Exemple #7
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'])
Exemple #8
0
def spid_logout(request, config_loader_path=None, **kwargs):
    """SAML Logout Request initiator

    This view initiates the SAML2 Logout request
    using the pysaml2 library to create the LogoutRequest.
    """
    state = StateCache(request.session)
    conf = get_config(config_loader_path, request)

    client = Saml2Client(conf, state_cache=state,
                         identity_cache=IdentityCache(request.session))
    subject_id = _get_subject_id(request.session)
    if subject_id is None:
        logger.warning(
            'The session does not contain the subject id for user %s',
            request.user)
        logger.error("Looks like the user %s is not logged in any IdP/AA", subject_id)
        return HttpResponseBadRequest("You are not logged in any IdP/AA")

    slo_req = saml2.samlp.LogoutRequest()

    binding = settings.SPID_DEFAULT_BINDING
    location_fixed = subject_id.name_qualifier
    location = location_fixed
    slo_req.destination = location_fixed
    # spid-testenv2 preleva l'attribute consumer service dalla authnRequest (anche se questo sta già nei metadati...)
    slo_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"
    slo_req.issuer = issuer

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

    # oggetto
    slo_req.name_id = subject_id


    session_info = client.users.get_info_from(slo_req.name_id,
                                              subject_id.name_qualifier,
                                              False)
    session_indexes = [session_info['session_index']]

    # aggiungere session index
    if session_indexes:
        sis = []
        for si in session_indexes:
            if isinstance(si, saml2.samlp.SessionIndex):
                sis.append(si)
            else:
                sis.append(saml2.samlp.SessionIndex(text=si))
        slo_req.session_index = sis

    slo_req.protocol_binding = binding

    assertion_consumer_service_url = client.config._sp_endpoints['assertion_consumer_service'][0][0]
    slo_req.assertion_consumer_service_url = assertion_consumer_service_url

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


    _req_str = slo_req_signed
    logger.debug('LogoutRequest to {}: {}'.format(subject_id.name_qualifier,
                                                  repr_saml(_req_str)))

    # get slo from metadata
    slo_location = None
    # for k,v in client.metadata.metadata.items():
        # idp_nq = v.entity.get(subject_id.name_qualifier)
        # if idp_nq:
            # slo_location = idp_nq['idpsso_descriptor'][0]['single_logout_service'][0]['location']

    slo_location = client.metadata.single_logout_service(subject_id.name_qualifier,
                                                         binding,
                                                         "idpsso")[0]['location']
    if not slo_location:
        logger.error('Unable to know SLO endpoint in {}'.format(subject_id.name_qualifier))
        return HttpResponse(text_type(e))

    http_info = client.apply_binding(binding,
                                     _req_str,
                                     slo_location,
                                     sign=True,
                                     sigalg=settings.SPID_ENC_ALG)

    state.sync()
    return HttpResponse(http_info['data'])
Exemple #9
0
def spid_sp_authn_request(conf,
                          selected_idp,
                          binding,
                          name_id_format,
                          authn_context,
                          sig_alg,
                          dig_alg,
                          next_url=''):
    client = Saml2Client(conf)

    logger.debug(f'Redirecting user to the IdP via {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)

    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"

    # issuer
    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 = name_id_format  # settings.SPID_NAMEID_FORMAT
    authn_req.name_id_policy = name_id_policy

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

    # force_auth = true only if SpidL >= 2
    # if 'SpidL1' in authn_context.authn_context_class_ref[0].text:
    # force_authn = 'false'
    # else:
    force_authn = 'true'
    authn_req.force_authn = force_authn
    # end force authn

    # settings.SPID_DEFAULT_BINDING
    authn_req.protocol_binding = binding

    assertion_consumer_service_url = client.config._sp_endpoints[
        'assertion_consumer_service'][0][0]
    authn_req.assertion_consumer_service_url = assertion_consumer_service_url

    authn_req_signed = client.sign(
        authn_req,
        sign_prepare=False,
        sign_alg=sig_alg,
        digest_alg=dig_alg,
    )
    logger.debug(f'AuthRequest to {selected_idp}: {authn_req_signed}')
    relay_state = next_url or reverse('djangosaml2:saml2_echo_attributes')
    http_info = client.apply_binding(binding,
                                     authn_req_signed,
                                     location,
                                     sign=True,
                                     sigalg=sig_alg,
                                     relay_state=relay_state)
    return dict(http_response=http_info,
                authn_request=authn_req_signed,
                relay_state=relay_state,
                session_id=authn_req.id)
Exemple #10
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
            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:
        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
Exemple #11
0
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
    enabled in the settings.py
    """
    attribute_mapping = attribute_mapping or get_custom_setting(
        'SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
    create_unknown_user = create_unknown_user if create_unknown_user is not None else \
                          get_custom_setting('SAML_CREATE_UNKNOWN_USER', True)
    conf = get_config(config_loader_path, request)
    try:
        xmlstr = request.POST['SAMLResponse']
    except KeyError:
        logger.warning('Missing "SAMLResponse" parameter in POST data.')
        raise SuspiciousOperation

    client = Saml2Client(conf, identity_cache=IdentityCache(request.session))

    oq_cache = OutstandingQueriesCache(request.session)
    outstanding_queries = oq_cache.outstanding_queries()

    try:
        response = client.parse_authn_request_response(xmlstr,
                                                       BINDING_HTTP_POST,
                                                       outstanding_queries)
    except (StatusError, ToEarly):
        logger.exception("Error processing SAML Assertion.")
        return fail_acs_response(request)
    except ResponseLifetimeExceed:
        logger.info(
            "SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.",
            exc_info=True)
        return fail_acs_response(request)
    except SignatureError:
        logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
        return fail_acs_response(request)
    except StatusAuthnFailed:
        logger.info("Authentication denied for user by IdP.", exc_info=True)
        return fail_acs_response(request)
    except StatusRequestDenied:
        logger.warning("Authentication interrupted at IdP.", exc_info=True)
        return fail_acs_response(request)
    except MissingKey:
        logger.exception(
            "SAML Identity Provider is not configured correctly: certificate key is missing!"
        )
        return fail_acs_response(request)
    except UnsolicitedResponse:
        logger.exception(
            "Received SAMLResponse when no request has been made.")
        return fail_acs_response(request)

    if response is None:
        logger.warning("Invalid SAML Assertion received (unknown error).")
        return fail_acs_response(request,
                                 status=400,
                                 exc_class=SuspiciousOperation)

    session_id = response.session_id()
    oq_cache.delete(session_id)

    # authenticate the remote user
    session_info = response.session_info()

    if callable(attribute_mapping):
        attribute_mapping = attribute_mapping()
    if callable(create_unknown_user):
        create_unknown_user = create_unknown_user()

    logger.debug('Trying to authenticate the user. Session info: %s',
                 session_info)
    user = auth.authenticate(request=request,
                             session_info=session_info,
                             attribute_mapping=attribute_mapping,
                             create_unknown_user=create_unknown_user)
    if user is None:
        logger.warning(
            "Could not authenticate user received in SAML Assertion. Session info: %s",
            session_info)
        raise PermissionDenied

    auth.login(request, user)
    _set_subject_id(request.session, session_info['name_id'])
    logger.debug("User %s authenticated via SSO.", user)

    logger.debug('Sending the post_authenticated signal')
    post_authenticated.send_robust(sender=user, session_info=session_info)

    # redirect the user to the view where he came from
    default_relay_state = get_custom_setting('ACS_DEFAULT_REDIRECT_URL',
                                             settings.LOGIN_REDIRECT_URL)
    relay_state = request.POST.get('RelayState', default_relay_state)
    if not relay_state:
        logger.warning('The RelayState parameter exists but is empty')
        relay_state = default_relay_state
    if not is_safe_url_compat(url=relay_state,
                              allowed_hosts={request.get_host()}):
        relay_state = settings.LOGIN_REDIRECT_URL
    logger.debug('Redirecting to the RelayState: %s', relay_state)
    return HttpResponseRedirect(relay_state)
Exemple #12
0
import base64
import requests
import saml2

from saml2 import saml, BINDING_HTTP_POST

from django.test.client import RequestFactory
from djangosaml2.conf import get_config
from djangosaml2.overrides import Saml2Client
from djangosaml2.utils import available_idps

# SP init
#########
conf = get_config(None)
client = Saml2Client(conf)
# just needed to initialize the MetadataStore - it automatically fetches remote idp's metadata
configured_idps = available_idps(conf)

# Arguments needed to create an Attribute query
###############################################
message_id = 'MSG_ID1'
entityid = "http://idp1.testunical.it:9000/idp/aa/metadata"
destination = "http://idp1.testunical.it:9000/aap"
subject_id = "E8042FB4-4D5B-48C3-8E14-8EDD852790DD"
attributes = {
    ('urn:oasis:names:tc:SAML:attribute:pairwise-id', "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"):
    "*****@*****.**",
    ("fiscalCode", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"):
    "TIN-SDF7SD89F7SD98F",
    ("email", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic", "email"):
    None,