4040from django .utils .six import text_type , binary_type , PY3
4141from django .views .decorators .csrf import csrf_exempt
4242
43- from saml2 import BINDING_HTTP_REDIRECT , BINDING_HTTP_POST
43+ from saml2 import (
44+ ecp , create_class_from_xml_string ,
45+ BINDING_HTTP_REDIRECT , BINDING_HTTP_POST ,
46+ )
47+ from saml2 .client import Saml2Client
48+ from saml2 .client_base import MIME_PAOS
4449from saml2 .metadata import entity_descriptor
4550from saml2 .ident import code , decode
4651from saml2 .sigver import MissingKey
52+ from saml2 .ecp_client import PAOS_HEADER_INFO
53+ from saml2 .profile .ecp import RelayState
4754from saml2 .s_utils import UnsupportedBinding
4855from saml2 .response import StatusError , StatusAuthnFailed , SignatureError , StatusRequestDenied
4956from saml2 .validate import ResponseLifetimeExceed , ToEarly
5259from djangosaml2 .cache import IdentityCache , OutstandingQueriesCache
5360from djangosaml2 .cache import StateCache
5461from djangosaml2 .conf import get_config
55- from djangosaml2 .overrides import Saml2Client
5662from djangosaml2 .signals import post_authenticated
5763from djangosaml2 .utils import (
5864 available_idps , fail_acs_response , get_custom_setting ,
5965 get_idp_sso_supported_bindings , get_location , is_safe_url_compat ,
66+ XmlResponse , SoapFaultResponse
6067)
6168
6269
@@ -104,7 +111,13 @@ def login(request,
104111 If set to None or nonexistent template, default form from the saml2 library
105112 will be rendered.
106113 """
107- logger .debug ('Login process started' )
114+ is_ecp = ("HTTP_PAOS" in request .META and
115+ request .META ["HTTP_PAOS" ] == PAOS_HEADER_INFO and
116+ MIME_PAOS in request .META ["HTTP_ACCEPT" ])
117+ if is_ecp :
118+ logger .debug ('ECP login process started' )
119+ else :
120+ logger .debug ('Login process started' )
108121
109122 came_from = request .GET .get ('next' , settings .LOGIN_REDIRECT_URL )
110123 if not came_from :
@@ -129,11 +142,15 @@ def login(request,
129142 redirect_authenticated_user = getattr (settings , 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN' , True )
130143 if redirect_authenticated_user :
131144 return HttpResponseRedirect (came_from )
145+ elif is_ecp :
146+ return HttpResponse ()
132147 else :
133148 logger .debug ('User is already logged in' )
134- return render (request , authorization_error_template , {
135- 'came_from' : came_from ,
136- })
149+ return render (
150+ request ,
151+ authorization_error_template ,
152+ {'came_from' : came_from , }
153+ )
137154
138155 selected_idp = request .GET .get ('idp' , None )
139156 conf = get_config (config_loader_path , request )
@@ -142,10 +159,14 @@ def login(request,
142159 idps = available_idps (conf )
143160 if selected_idp is None and len (idps ) > 1 :
144161 logger .debug ('A discovery process is needed' )
145- return render (request , wayf_template , {
162+ return render (
163+ request ,
164+ wayf_template ,
165+ {
146166 'available_idps' : idps .items (),
147167 'came_from' : came_from ,
148- })
168+ }
169+ )
149170
150171 # choose a binding to try first
151172 sign_requests = getattr (conf , '_sp_authn_requests_signed' , False )
@@ -171,9 +192,37 @@ def login(request,
171192 selected_idp , BINDING_HTTP_POST , BINDING_HTTP_REDIRECT )
172193
173194 client = Saml2Client (conf )
195+ try :
196+ if is_ecp :
197+ (session_id , result ) = ecp .ecp_auth_request (
198+ cls = client ,
199+ entityid = None ,
200+ relay_state = came_from
201+ )
202+ if not session_id > 0 :
203+ logger .error ("Error in ECP auth request." )
204+ else :
205+ (session_id , result ) = client .prepare_for_authenticate (
206+ entityid = selected_idp , relay_state = came_from ,
207+ binding = binding ,
208+ )
209+ except TypeError as e :
210+ message = 'Unable to know which IdP to use'
211+ logger .error (message )
212+ if is_ecp :
213+ return SoapFaultResponse (message , status = 400 )
214+ return HttpResponseBadRequest (message )
215+
216+ logger .debug ('Saving the session_id in the OutstandingQueries cache' )
217+ oq_cache = OutstandingQueriesCache (request .session )
218+ oq_cache .set (session_id , came_from )
219+
220+ if is_ecp :
221+ logger .debug ('Redirecting the ECP client to the IdP' )
222+ return XmlResponse (result )
174223 http_response = None
224+ logger .debug ('Redirecting user to the IdP via %s binding.' , binding .split (':' )[- 1 ])
175225
176- logger .debug ('Redirecting user to the IdP via %s binding.' , binding )
177226 if binding == BINDING_HTTP_REDIRECT :
178227 try :
179228 # do not sign the xml itself, instead use the sigalg to
@@ -252,45 +301,65 @@ def assertion_consumer_service(request,
252301 djangosaml2.backends.Saml2Backend that should be
253302 enabled in the settings.py
254303 """
255- attribute_mapping = attribute_mapping or get_custom_setting ('SAML_ATTRIBUTE_MAPPING' , {'uid' : ('username' , )})
256- create_unknown_user = create_unknown_user if create_unknown_user is not None else \
257- get_custom_setting ('SAML_CREATE_UNKNOWN_USER' , True )
258- conf = get_config (config_loader_path , request )
259- try :
260- xmlstr = request .POST ['SAMLResponse' ]
261- except KeyError :
262- logger .warning ('Missing "SAMLResponse" parameter in POST data.' )
263- raise SuspiciousOperation
304+ is_ecp = MIME_PAOS == request .META ["CONTENT_TYPE" ]
305+
306+ attribute_mapping = attribute_mapping or get_custom_setting (
307+ 'SAML_ATTRIBUTE_MAPPING' , {'uid' : ('username' , )})
308+ create_unknown_user = create_unknown_user or get_custom_setting (
309+ 'SAML_CREATE_UNKNOWN_USER' , True )
310+ logger .debug ('Assertion Consumer Service started' )
264311
312+ conf = get_config (config_loader_path , request )
265313 client = Saml2Client (conf , identity_cache = IdentityCache (request .session ))
266314
315+ if is_ecp :
316+ data = client .unpack_soap_message (request .body )
317+ relay_state_found = False
318+ for header in data ["header" ]:
319+ inst = create_class_from_xml_string (RelayState , header )
320+ if isinstance (inst , RelayState ):
321+ relay_state_found = True
322+ if not relay_state_found :
323+ return SoapFaultResponse ('Couldn\' t find RelayState data.' ,
324+ status = 400 )
325+ xmlstr = data ["body" ]
326+ else :
327+ if 'SAMLResponse' not in request .POST :
328+ return HttpResponseBadRequest (
329+ 'Couldn\' t find "SAMLResponse" in POST data.' )
330+ xmlstr = request .POST ['SAMLResponse' ]
331+
267332 oq_cache = OutstandingQueriesCache (request .session )
268333 outstanding_queries = oq_cache .outstanding_queries ()
269334
270335 try :
271- response = client .parse_authn_request_response (xmlstr , BINDING_HTTP_POST , outstanding_queries )
336+ # process the authentication response
337+ binding = None if is_ecp else BINDING_HTTP_POST
338+ response = client .parse_authn_request_response (xmlstr , binding ,
339+ outstanding_queries )
272340 except (StatusError , ToEarly ):
273341 logger .exception ("Error processing SAML Assertion." )
274- return fail_acs_response (request )
342+ return fail_acs_response (request , soap = is_ecp )
275343 except ResponseLifetimeExceed :
276344 logger .info ("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack." , exc_info = True )
277- return fail_acs_response (request )
345+ return fail_acs_response (request , soap = is_ecp )
278346 except SignatureError :
279347 logger .info ("Invalid or malformed SAML Assertion." , exc_info = True )
280- return fail_acs_response (request )
348+ return fail_acs_response (request , soap = is_ecp )
281349 except StatusAuthnFailed :
282350 logger .info ("Authentication denied for user by IdP." , exc_info = True )
283- return fail_acs_response (request )
351+ return fail_acs_response (request , soap = is_ecp )
284352 except StatusRequestDenied :
285353 logger .warning ("Authentication interrupted at IdP." , exc_info = True )
286- return fail_acs_response (request )
354+ return fail_acs_response (request , soap = is_ecp )
287355 except MissingKey :
288356 logger .exception ("SAML Identity Provider is not configured correctly: certificate key is missing!" )
289- return fail_acs_response (request )
357+ return fail_acs_response (request , soap = is_ecp )
290358
291359 if response is None :
292360 logger .warning ("Invalid SAML Assertion received (unknown error)." )
293- return fail_acs_response (request , status = 400 , exc_class = SuspiciousOperation )
361+ return fail_acs_response (request , status = 400 ,
362+ exc_class = SuspiciousOperation , soap = is_ecp )
294363
295364 session_id = response .session_id ()
296365 oq_cache .delete (session_id )
@@ -309,6 +378,10 @@ def assertion_consumer_service(request,
309378 attribute_mapping = attribute_mapping ,
310379 create_unknown_user = create_unknown_user )
311380 if user is None :
381+ message = 'The user is None'
382+ logger .error (message )
383+ if is_ecp :
384+ return SoapFaultResponse (message , status = 403 )
312385 logger .warning ("Could not authenticate user received in SAML Assertion. Session info: %s" , session_info )
313386 raise PermissionDenied
314387
@@ -412,7 +485,7 @@ def logout_service_post(request, *args, **kwargs):
412485
413486
414487def do_logout_service (request , data , binding , config_loader_path = None , next_page = None ,
415- logout_error_template = 'djangosaml2/logout_error.html' ):
488+ logout_error_template = 'djangosaml2/logout_error.html' ):
416489 """SAML Logout Response endpoint
417490
418491 The IdP will send the logout response to this view,
@@ -500,4 +573,5 @@ def register_namespace_prefixes():
500573 for prefix , namespace in prefixes :
501574 ElementTree ._namespace_map [namespace ] = prefix
502575
576+
503577register_namespace_prefixes ()
0 commit comments