Source code for saml2.logout_response
# -*- coding: utf-8 -*-
# Copyright (c) 2010-2018 OneLogin, Inc.
# MIT License
from base64 import b64decode
from datetime import datetime
from lxml import etree
from urllib import quote_plus
from xml.dom.minidom import Document, parseString
import dm.xmlsec.binding as xmlsec
from saml2.constants import OneLogin_Saml2_Constants
from saml2.utils import OneLogin_Saml2_Utils
[docs]class OneLogin_Saml2_Logout_Response():
    def __init__(self, settings, response=None):
        """
        Constructs a Logout Response object (Initialize params from settings
        and if provided load the Logout Response.
        Arguments are:
            * (OneLogin_Saml2_Settings)   settings. Setting data
            * (string)                    response. An UUEncoded SAML Logout
                                                    response from the IdP.
        """
        self.__settings = settings
        if response is not None:
            self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response)
            self.document = parseString(self.__logout_response)
[docs]    def get_issuer(self):
        """
        Gets the Issuer of the Logout Response Message
        :return: The Issuer
        :rtype: string
        """
        issuer = None
        issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer')
        if len(issuer_nodes) == 1:
            issuer = issuer_nodes[0].text
        return issuer
 
[docs]    def get_status(self):
        """
        Gets the Status
        :return: The Status
        :rtype: string
        """
        entries = self.__query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode')
        if len(entries) == 0:
            return None
        status = entries[0].attrib['Value']
        return status
 
[docs]    def is_valid(self, request_data, request_id=None):
        """
        Determines if the SAML LogoutResponse is valid
        :param request_id: The ID of the LogoutRequest sent by this SP to the IdP
        :type request_id: string
        :return: Returns if the SAML LogoutResponse is or not valid
        :rtype: boolean
        """
        try:
            idp_data = self.__settings.get_idp_data()
            idp_entity_id = idp_data['entityId']
            get_data = request_data['get_data']
            if self.__settings.is_strict():
                res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
                if not isinstance(res, Document):
                    raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
                security = self.__settings.get_security_data()
                # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided
                if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'):
                    in_response_to = self.document.documentElement.getAttribute('InResponseTo')
                    if request_id != in_response_to:
                        raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id))
                # Check issuer
                issuer = self.get_issuer()
                if issuer is None or issuer != idp_entity_id:
                    raise Exception('Invalid issuer in the Logout Request')
                current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
                # Check destination
                if self.document.documentElement.hasAttribute('Destination'):
                    destination = self.document.documentElement.getAttribute('Destination')
                    if destination is not None:
                        if current_url not in destination:
                            raise Exception('The LogoutRequest was received at $currentURL instead of $destination')
                if security['wantMessagesSigned']:
                    if 'Signature' not in get_data:
                        raise Exception('The Message of the Logout Response is not signed and the SP require it')
            if 'Signature' in get_data:
                if 'SigAlg' not in get_data:
                    sign_alg = OneLogin_Saml2_Constants.RSA_SHA1
                else:
                    sign_alg = get_data['SigAlg']
                if sign_alg != OneLogin_Saml2_Constants.RSA_SHA1:
                    raise Exception('Invalid signAlg in the recieved Logout Response')
                signed_query = 'SAMLResponse=%s' % quote_plus(get_data['SAMLResponse'])
                if 'RelayState' in get_data:
                    signed_query = '%s&RelayState=%s' % (signed_query, quote_plus(get_data['RelayState']))
                signed_query = '%s&SigAlg=%s' % (signed_query, quote_plus(sign_alg))
                if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
                    raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required')
                cert = idp_data['x509cert']
                xmlsec.initialize()
                objkey = xmlsec.Key.load(cert, xmlsec.KeyDataFormatPem, None)  # FIXME is this right?
                if not objkey.verifySignature(signed_query, b64decode(get_data['Signature'])):
                    raise Exception('Signature validation failed. Logout Response rejected')
            return True
        except Exception as e:
            debug = self.__settings.is_debug_active()
            if debug:
                print(e.strerror)
            return False
 
    def __query(self, query):
        """
        Extracts a node from the DOMDocument (Logout Response Menssage)
        :param query: Xpath Expresion
        :type query: string
        :return: The queried node
        :rtype: DOMNodeList
        """
        # Switch to lxml for querying
        xml = self.document.toxml()
        return OneLogin_Saml2_Utils.query(etree.fromstring(xml), query)
[docs]    def build(self, in_response_to):
        """
        Creates a Logout Response object.
        :param in_response_to: InResponseTo value for the Logout Response.
        :type in_response_to: string
        """
        sp_data = self.__settings.get_sp_data()
        idp_data = self.__settings.get_idp_data()
        uid = OneLogin_Saml2_Utils.generate_unique_id()
        issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(
            int(datetime.now().strftime("%s"))
        )
        logout_response = """<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                      ID="%(id)s"
                      Version="2.0"
                      IssueInstant="%(issue_instant)s"
                      Destination="%(destination)s"
                      InResponseTo="%(in_response_to)s"
>
    <saml:Issuer>%(entity_id)s</saml:Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </samlp:Status>
</samlp:LogoutResponse>""" % {
            'id': uid,
            'issue_instant': issue_instant,
            'destination': idp_data['singleLogoutService']['url'],
            'in_response_to': in_response_to,
            'entity_id': sp_data['entityId'],
        }
        self.__logout_response = logout_response
 
[docs]    def get_response(self):
        """
        Returns a Logout Response object.
        :return: Logout Response deflated and base64 encoded
        :rtype: string
        """
        return OneLogin_Saml2_Utils.deflate_and_base64_encode(self.__logout_response)