44from datetime import datetime , timedelta
55
66from onelogin .saml import SignatureVerifier
7+ from onelogin .saml .Utils import get_self_url_no_query
78
89
910namespaces = dict (
1011 samlp = 'urn:oasis:names:tc:SAML:2.0:protocol' ,
1112 saml = 'urn:oasis:names:tc:SAML:2.0:assertion' ,
13+ ds = 'http://www.w3.org/2000/09/xmldsig#' ,
1214)
1315
1416
@@ -38,8 +40,36 @@ def __init__(self, msg):
3840 def __str__ (self ):
3941 return '%s: %s' % (self .__doc__ , self ._msg )
4042
43+
44+ class ResponseFormatError (Exception ):
45+ """There was a problem validating the format"""
46+ def __init__ (self , msg ):
47+ self ._msg = msg
48+
49+ def __str__ (self ):
50+ return '%s: %s' % (self .__doc__ , self ._msg )
51+
52+
53+ class ResponseDestinationError (Exception ):
54+ """There was a problem validating a destination"""
55+ def __init__ (self , msg ):
56+ self ._msg = msg
57+
58+ def __str__ (self ):
59+ return '%s: %s' % (self .__doc__ , self ._msg )
60+
61+
62+ class ResponseSubjectConfirmationError (Exception ):
63+ """There was a problem validating the response, no valid SubjectConfirmation found"""
64+ def __init__ (self , msg ):
65+ self ._msg = msg
66+
67+ def __str__ (self ):
68+ return '%s: %s' % (self .__doc__ , self ._msg )
69+
70+
4171class Response (object ):
42- def __init__ (self , response , signature , _base64 = None , _etree = None , issuer = None ):
72+ def __init__ (self , request_data , response , signature , _base64 = None , _etree = None , issuer = None ):
4373 """
4474 Extract information from an samlp:Response
4575 Arguments:
@@ -51,6 +81,7 @@ def __init__(self, response, signature, _base64=None, _etree=None, issuer=None):
5181 if _etree is None :
5282 _etree = etree
5383
84+ self ._request_data = request_data
5485 decoded_response = _base64 .b64decode (response )
5586 self ._document = _etree .fromstring (decoded_response )
5687 self ._signature = signature
@@ -93,11 +124,58 @@ def get_assertion_attribute_value(self, attribute_name):
93124 result = self ._document .xpath ('/samlp:Response/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name="%s"]/saml:AttributeValue' % attribute_name , namespaces = namespaces )
94125 return [n .text .strip () for n in result ]
95126
127+ def get_audiences (self ):
128+ """
129+ Gets the audiences
130+
131+ :returns: The valid audiences for the SAML Response
132+ :rtype: list
133+ """
134+ audiences = []
135+
136+ audience_nodes = self ._document .xpath (
137+ '/samlp:Response/saml:Assertion/saml:Conditions/saml:AudienceRestriction/saml:Audience' ,
138+ namespaces = namespaces ,
139+ )
140+ for audience_node in audience_nodes :
141+ audiences .append (audience_node .text )
142+ return audiences
143+
144+ def validate_num_assertions (self ):
145+ """
146+ Verifies that the document only contains a single Assertion (encrypted or not)
147+
148+ :returns: True if only 1 assertion encrypted or not
149+ :rtype: bool
150+ """
151+ #Not encrypted assertion supported yet
152+ #encrypted_assertion_nodes = self._document.xpath('//saml:EncryptedAssertion')
153+ assertion_nodes = self ._document .xpath (
154+ '//saml:Assertion' ,
155+ namespaces = namespaces ,
156+ )
157+ return len (assertion_nodes ) == 1
158+
96159 def is_valid (self , _clock = None , _verifier = None ):
97160 """
98161 Verify that the samlp:Response is valid.
99162 Return True if valid, otherwise False.
100163 """
164+
165+ # Checks SAML version
166+ if self ._document .get ('Version' , None ) != '2.0' :
167+ raise ResponseFormatError ('Unsupported SAML version' )
168+
169+ # Checks that ID exists
170+ if self ._document .get ('ID' , None ) is None :
171+ raise ResponseFormatError ('Missing ID attribute on SAML Response' )
172+
173+ # Checks that the response only has one assertion
174+ if not self .validate_num_assertions ():
175+ raise ResponseFormatError ('Only 1 Assertion in the SAMLResponse is supported' )
176+
177+
178+
101179 if _clock is None :
102180 _clock = datetime .utcnow
103181 if _verifier is None :
@@ -110,44 +188,74 @@ def is_valid(self, _clock=None, _verifier=None):
110188
111189 now = _clock ()
112190
113- foundCondition = False
114- fountConditionAndAudience = False
115-
116191 for condition in conditions :
117192
118193 not_before = condition .attrib .get ('NotBefore' , None )
119194 not_on_or_after = condition .attrib .get ('NotOnOrAfter' , None )
120195
121196 if not_before is None :
122- #notbefore condition is not mandatory. If it is not specified, use yesterday as not_before condition
123197 not_before = (now - timedelta (0 , 5 , 0 )).strftime ('%Y-%m-%dT%H:%M:%SZ' )
124198 if not_on_or_after is None :
125- continue
199+ not_on_or_after = ( now + timedelta ( 0 , 5 , 0 )). strftime ( '%Y-%m-%dT%H:%M:%SZ' )
126200
127201 not_before = self ._parse_datetime (not_before )
128202 not_on_or_after = self ._parse_datetime (not_on_or_after )
129203
130204 if now < not_before :
131- continue
205+ raise ResponseConditionError ( 'Timmig issue' )
132206 if now >= not_on_or_after :
207+ raise ResponseConditionError ('Timmig issue' )
208+
209+ current_url = get_self_url_no_query (self ._request_data )
210+
211+ # Checks destination
212+ destination = self ._document .get ('Destination' , None )
213+ if destination and destination not in current_url :
214+ raise ResponseDestinationError ('The response was received at %s instead of %s' % (current_url , destination ))
215+
216+ # Checks audience
217+ valid_audiences = self .get_audiences ()
218+ if valid_audiences and self ._issuer not in valid_audiences :
219+ raise ResponseConditionError ('%s is not a valid audience for this Response' % self ._issuer )
220+
221+ # Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid
222+ any_subject_confirmation = False
223+ subject_confirmation_nodes = self ._document .xpath (
224+ '//saml:Subject/saml:SubjectConfirmation' ,
225+ namespaces = namespaces
226+ )
227+
228+ in_response_to = self ._document .get ('InResponseTo' , None )
229+ for scn in subject_confirmation_nodes :
230+ method = scn .get ('Method' , None )
231+ if method and method != 'urn:oasis:names:tc:SAML:2.0:cm:bearer' :
133232 continue
134- foundCondition = True
135-
136- if self ._issuer :
137- audiences = condition .xpath (
138- '/samlp:Response/saml:Assertion/saml:Conditions/saml:AudienceRestriction/saml:Audience' ,
139- namespaces = namespaces ,
140- )
141- audienceValues = []
142- for audience in audiences :
143- audienceValues .append (audience .text )
144- if self ._issuer in audienceValues :
145- fountConditionAndAudience = True
146-
147- if not foundCondition :
148- raise ResponseConditionError ('Timmig issue' )
149- if foundCondition and not fountConditionAndAudience :
150- raise ResponseConditionError ('Not valid Audience' )
233+ scData = scn .find ('saml:SubjectConfirmationData' , namespaces = namespaces )
234+ if scData is None :
235+ continue
236+ else :
237+ irt = scData .get ('InResponseTo' , None )
238+ if irt != in_response_to :
239+ continue
240+ recipient = scData .get ('Recipient' , None )
241+ if recipient not in current_url :
242+ continue
243+ nooa = scData .get ('NotOnOrAfter' , None )
244+ if nooa :
245+ parsed_nooa = self ._parse_datetime (nooa )
246+ if parsed_nooa <= now :
247+ continue
248+ nb = scData .get ('NotBefore' , None )
249+ if nb :
250+ parsed_nb = self ._parse_datetime (nb )
251+ if parsed_nb > now :
252+ continue
253+ any_subject_confirmation = True
254+ break
255+
256+ if not any_subject_confirmation :
257+ raise ResponseSubjectConfirmationError ('A valid SubjectConfirmation was not found on this Response' )
258+
151259
152260 return _verifier (
153261 self ._document ,
0 commit comments