# -*- coding: utf-8 -*-
""" OneLogin_Saml2_Logout_Request class
Logout Request class of SAML Python Toolkit.
"""
from onelogin.saml2 import compat
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils, OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
from onelogin.saml2.xml_templates import OneLogin_Saml2_Templates
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
[docs]class OneLogin_Saml2_Logout_Request(object):
"""
This class handles a Logout Request.
Builds a Logout Response object and validates it.
"""
def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None, spnq=None):
"""
Constructs the Logout Request object.
:param settings: Setting data
:type settings: OneLogin_Saml2_Settings
:param request: Optional. A LogoutRequest to be loaded instead build one.
:type request: string
:param name_id: The NameID that will be set in the LogoutRequest.
:type name_id: string
:param session_index: SessionIndex that identifies the session of the user.
:type session_index: string
:param nq: IDP Name Qualifier
:type: string
:param name_id_format: The NameID Format that will be set in the LogoutRequest.
:type: string
:param spnq: SP Name Qualifier
:type: string
"""
self._settings = settings
self._error = None
self.id = None
if request is None:
sp_data = self._settings.get_sp_data()
idp_data = self._settings.get_idp_data()
security = self._settings.get_security_data()
self.id = self._generate_request_id()
issue_instant = OneLogin_Saml2_Utils.parse_time_to_SAML(OneLogin_Saml2_Utils.now())
cert = None
if security['nameIdEncrypted']:
exists_multix509enc = 'x509certMulti' in idp_data and \
'encryption' in idp_data['x509certMulti'] and \
idp_data['x509certMulti']['encryption']
if exists_multix509enc:
cert = idp_data['x509certMulti']['encryption'][0]
else:
cert = self._settings.get_idp_cert()
if name_id is not None:
if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
name_id_format = sp_data['NameIDFormat']
else:
name_id = idp_data['entityId']
name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY
# From saml-core-2.0-os 8.3.6, when the entity Format is used:
# "The NameQualifier, SPNameQualifier, and SPProvidedID attributes
# MUST be omitted.
if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
nq = None
spnq = None
# NameID Format UNSPECIFIED omitted
if name_id_format and name_id_format == OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
name_id_format = None
name_id_obj = OneLogin_Saml2_Utils.generate_name_id(
name_id,
spnq,
name_id_format,
cert,
False,
nq
)
if session_index:
session_index_str = '<samlp:SessionIndex>%s</samlp:SessionIndex>' % session_index
else:
session_index_str = ''
logout_request = OneLogin_Saml2_Templates.LOGOUT_REQUEST % \
{
'id': self.id,
'issue_instant': issue_instant,
'single_logout_url': self._settings.get_idp_slo_url(),
'entity_id': sp_data['entityId'],
'name_id': name_id_obj,
'session_index': session_index_str,
}
else:
logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(request, ignore_zip=True)
self.id = self.get_id(logout_request)
self._logout_request = compat.to_string(logout_request)
[docs] def get_request(self, deflate=True):
"""
Returns the Logout Request deflated, base64encoded
:param deflate: It makes the deflate process optional
:type: bool
:return: Logout Request maybe deflated and base64 encoded
:rtype: str object
"""
if deflate:
request = OneLogin_Saml2_Utils.deflate_and_base64_encode(self._logout_request)
else:
request = OneLogin_Saml2_Utils.b64encode(self._logout_request)
return request
[docs] def get_xml(self):
"""
Returns the XML that will be sent as part of the request
or that was received at the SP
:return: XML request body
:rtype: string
"""
return self._logout_request
[docs] @classmethod
def get_id(cls, request):
"""
Returns the ID of the Logout Request
:param request: Logout Request Message
:type request: string|DOMDocument
:return: string ID
:rtype: str object
"""
elem = OneLogin_Saml2_XML.to_etree(request)
return elem.get('ID', None)
[docs] @classmethod
def get_nameid_data(cls, request, key=None):
"""
Gets the NameID Data of the the Logout Request
:param request: Logout Request Message
:type request: string|DOMDocument
:param key: The SP key
:type key: string
:return: Name ID Data (Value, Format, NameQualifier, SPNameQualifier)
:rtype: dict
"""
elem = OneLogin_Saml2_XML.to_etree(request)
name_id = None
encrypted_entries = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID')
if len(encrypted_entries) == 1:
if key is None:
raise OneLogin_Saml2_Error(
'Private Key is required in order to decrypt the NameID, check settings',
OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
)
encrypted_data_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
if len(encrypted_data_nodes) == 1:
encrypted_data = encrypted_data_nodes[0]
name_id = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key)
else:
entries = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:NameID')
if len(entries) == 1:
name_id = entries[0]
if name_id is None:
raise OneLogin_Saml2_ValidationError(
'NameID not found in the Logout Request',
OneLogin_Saml2_ValidationError.NO_NAMEID
)
name_id_data = {
'Value': OneLogin_Saml2_XML.element_text(name_id)
}
for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
if attr in name_id.attrib:
name_id_data[attr] = name_id.attrib[attr]
return name_id_data
[docs] @classmethod
def get_nameid(cls, request, key=None):
"""
Gets the NameID of the Logout Request Message
:param request: Logout Request Message
:type request: string|DOMDocument
:param key: The SP key
:type key: string
:return: Name ID Value
:rtype: string
"""
name_id = cls.get_nameid_data(request, key)
return name_id['Value']
[docs] @classmethod
def get_issuer(cls, request):
"""
Gets the Issuer of the Logout Request Message
:param request: Logout Request Message
:type request: string|DOMDocument
:return: The Issuer
:rtype: string
"""
elem = OneLogin_Saml2_XML.to_etree(request)
issuer = None
issuer_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/saml:Issuer')
if len(issuer_nodes) == 1:
issuer = OneLogin_Saml2_XML.element_text(issuer_nodes[0])
return issuer
[docs] @classmethod
def get_session_indexes(cls, request):
"""
Gets the SessionIndexes from the Logout Request
:param request: Logout Request Message
:type request: string|DOMDocument
:return: The SessionIndex value
:rtype: list
"""
elem = OneLogin_Saml2_XML.to_etree(request)
session_indexes = []
session_index_nodes = OneLogin_Saml2_XML.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex')
for session_index_node in session_index_nodes:
session_indexes.append(OneLogin_Saml2_XML.element_text(session_index_node))
return session_indexes
[docs] def is_valid(self, request_data, raise_exceptions=False):
"""
Checks if the Logout Request received is valid
:param request_data: Request Data
:type request_data: dict
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
:return: If the Logout Request is or not valid
:rtype: boolean
"""
self._error = None
try:
root = OneLogin_Saml2_XML.to_etree(self._logout_request)
idp_data = self._settings.get_idp_data()
idp_entity_id = idp_data['entityId']
get_data = ('get_data' in request_data and request_data['get_data']) or dict()
if self._settings.is_strict():
res = OneLogin_Saml2_XML.validate_xml(root, 'saml-schema-protocol-2.0.xsd', self._settings.is_debug_active())
if isinstance(res, str):
raise OneLogin_Saml2_ValidationError(
'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
)
security = self._settings.get_security_data()
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
# Check NotOnOrAfter
if root.get('NotOnOrAfter', None):
na = OneLogin_Saml2_Utils.parse_SAML_to_time(root.get('NotOnOrAfter'))
if na <= OneLogin_Saml2_Utils.now():
raise OneLogin_Saml2_ValidationError(
'Could not validate timestamp: expired. Check system clock.)',
OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED
)
# Check destination
destination = root.get('Destination', None)
if destination:
if not OneLogin_Saml2_Utils.normalize_url(url=destination).startswith(OneLogin_Saml2_Utils.normalize_url(url=current_url)):
raise OneLogin_Saml2_ValidationError(
'The LogoutRequest was received at '
'%(currentURL)s instead of %(destination)s' %
{
'currentURL': current_url,
'destination': destination,
},
OneLogin_Saml2_ValidationError.WRONG_DESTINATION
)
# Check issuer
issuer = self.get_issuer(root)
if issuer is not None and issuer != idp_entity_id:
raise OneLogin_Saml2_ValidationError(
'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' %
{
'idpEntityId': idp_entity_id,
'issuer': issuer
},
OneLogin_Saml2_ValidationError.WRONG_ISSUER
)
if security['wantMessagesSigned']:
if 'Signature' not in get_data:
raise OneLogin_Saml2_ValidationError(
'The Message of the Logout Request is not signed and the SP require it',
OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
)
return True
except Exception as err:
# pylint: disable=R0801
self._error = str(err)
debug = self._settings.is_debug_active()
if debug:
print(err)
if raise_exceptions:
raise
return False
[docs] def get_error(self):
"""
After executing a validation process, if it fails this method returns the cause
"""
return self._error
def _generate_request_id(self):
"""
Generate an unique logout request ID.
"""
return OneLogin_Saml2_Utils.generate_unique_id()