4949from django .utils .six import text_type , binary_type , PY3
5050from django .views .decorators .csrf import csrf_exempt
5151
52- from saml2 import BINDING_HTTP_REDIRECT , BINDING_HTTP_POST
52+ from saml2 import (
53+ ecp , create_class_from_xml_string ,
54+ BINDING_HTTP_REDIRECT , BINDING_HTTP_POST ,
55+ )
56+ from saml2 .client import Saml2Client
57+ from saml2 .client_base import MIME_PAOS
5358from saml2 .metadata import entity_descriptor
5459from saml2 .ident import code , decode
5560from saml2 .sigver import MissingKey
61+ from saml2 .ecp_client import PAOS_HEADER_INFO
62+ from saml2 .profile .ecp import RelayState
5663from saml2 .s_utils import UnsupportedBinding
5764from saml2 .response import StatusError , StatusAuthnFailed , SignatureError , StatusRequestDenied
5865from saml2 .validate import ResponseLifetimeExceed , ToEarly
6168from djangosaml2 .cache import IdentityCache , OutstandingQueriesCache
6269from djangosaml2 .cache import StateCache
6370from djangosaml2 .conf import get_config
64- from djangosaml2 .overrides import Saml2Client
6571from djangosaml2 .signals import post_authenticated
6672from djangosaml2 .utils import (
6773 available_idps , fail_acs_response , get_custom_setting ,
6874 get_idp_sso_supported_bindings , get_location , is_safe_url_compat ,
75+ XmlResponse , SoapFaultResponse
6976)
7077
7178
@@ -113,7 +120,13 @@ def login(request,
113120 If set to None or nonexistent template, default form from the saml2 library
114121 will be rendered.
115122 """
116- logger .debug ('Login process started' )
123+ is_ecp = ("HTTP_PAOS" in request .META and
124+ request .META ["HTTP_PAOS" ] == PAOS_HEADER_INFO and
125+ MIME_PAOS in request .META ["HTTP_ACCEPT" ])
126+ if is_ecp :
127+ logger .debug ('ECP login process started' )
128+ else :
129+ logger .debug ('Login process started' )
117130
118131 came_from = request .GET .get ('next' , settings .LOGIN_REDIRECT_URL )
119132 if not came_from :
@@ -138,11 +151,15 @@ def login(request,
138151 redirect_authenticated_user = getattr (settings , 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN' , True )
139152 if redirect_authenticated_user :
140153 return HttpResponseRedirect (came_from )
154+ elif is_ecp :
155+ return HttpResponse ()
141156 else :
142157 logger .debug ('User is already logged in' )
143- return render (request , authorization_error_template , {
144- 'came_from' : came_from ,
145- })
158+ return render (
159+ request ,
160+ authorization_error_template ,
161+ {'came_from' : came_from , }
162+ )
146163
147164 selected_idp = request .GET .get ('idp' , None )
148165 conf = get_config (config_loader_path , request )
@@ -151,10 +168,14 @@ def login(request,
151168 idps = available_idps (conf )
152169 if selected_idp is None and len (idps ) > 1 :
153170 logger .debug ('A discovery process is needed' )
154- return render (request , wayf_template , {
171+ return render (
172+ request ,
173+ wayf_template ,
174+ {
155175 'available_idps' : idps .items (),
156176 'came_from' : came_from ,
157- })
177+ }
178+ )
158179
159180 # choose a binding to try first
160181 sign_requests = getattr (conf , '_sp_authn_requests_signed' , False )
@@ -180,9 +201,37 @@ def login(request,
180201 selected_idp , BINDING_HTTP_POST , BINDING_HTTP_REDIRECT )
181202
182203 client = Saml2Client (conf )
204+ try :
205+ if is_ecp :
206+ (session_id , result ) = ecp .ecp_auth_request (
207+ cls = client ,
208+ entityid = None ,
209+ relay_state = came_from
210+ )
211+ if not session_id > 0 :
212+ logger .error ("Error in ECP auth request." )
213+ else :
214+ (session_id , result ) = client .prepare_for_authenticate (
215+ entityid = selected_idp , relay_state = came_from ,
216+ binding = binding ,
217+ )
218+ except TypeError as e :
219+ message = 'Unable to know which IdP to use'
220+ logger .error (message )
221+ if is_ecp :
222+ return SoapFaultResponse (message , status = 400 )
223+ return HttpResponseBadRequest (message )
224+
225+ logger .debug ('Saving the session_id in the OutstandingQueries cache' )
226+ oq_cache = OutstandingQueriesCache (request .session )
227+ oq_cache .set (session_id , came_from )
228+
229+ if is_ecp :
230+ logger .debug ('Redirecting the ECP client to the IdP' )
231+ return XmlResponse (result )
183232 http_response = None
233+ logger .debug ('Redirecting user to the IdP via %s binding.' , binding .split (':' )[- 1 ])
184234
185- logger .debug ('Redirecting user to the IdP via %s binding.' , binding )
186235 if binding == BINDING_HTTP_REDIRECT :
187236 try :
188237 # do not sign the xml itself, instead use the sigalg to
@@ -261,45 +310,65 @@ def assertion_consumer_service(request,
261310 djangosaml2.backends.Saml2Backend that should be
262311 enabled in the settings.py
263312 """
264- attribute_mapping = attribute_mapping or get_custom_setting ('SAML_ATTRIBUTE_MAPPING' , {'uid' : ('username' , )})
265- create_unknown_user = create_unknown_user if create_unknown_user is not None else \
266- get_custom_setting ('SAML_CREATE_UNKNOWN_USER' , True )
267- conf = get_config (config_loader_path , request )
268- try :
269- xmlstr = request .POST ['SAMLResponse' ]
270- except KeyError :
271- logger .warning ('Missing "SAMLResponse" parameter in POST data.' )
272- raise SuspiciousOperation
313+ is_ecp = MIME_PAOS == request .META ["CONTENT_TYPE" ]
314+
315+ attribute_mapping = attribute_mapping or get_custom_setting (
316+ 'SAML_ATTRIBUTE_MAPPING' , {'uid' : ('username' , )})
317+ create_unknown_user = create_unknown_user or get_custom_setting (
318+ 'SAML_CREATE_UNKNOWN_USER' , True )
319+ logger .debug ('Assertion Consumer Service started' )
273320
321+ conf = get_config (config_loader_path , request )
274322 client = Saml2Client (conf , identity_cache = IdentityCache (request .session ))
275323
324+ if is_ecp :
325+ data = client .unpack_soap_message (request .body )
326+ relay_state_found = False
327+ for header in data ["header" ]:
328+ inst = create_class_from_xml_string (RelayState , header )
329+ if isinstance (inst , RelayState ):
330+ relay_state_found = True
331+ if not relay_state_found :
332+ return SoapFaultResponse ('Couldn\' t find RelayState data.' ,
333+ status = 400 )
334+ xmlstr = data ["body" ]
335+ else :
336+ if 'SAMLResponse' not in request .POST :
337+ return HttpResponseBadRequest (
338+ 'Couldn\' t find "SAMLResponse" in POST data.' )
339+ xmlstr = request .POST ['SAMLResponse' ]
340+
276341 oq_cache = OutstandingQueriesCache (request .session )
277342 outstanding_queries = oq_cache .outstanding_queries ()
278343
279344 try :
280- response = client .parse_authn_request_response (xmlstr , BINDING_HTTP_POST , outstanding_queries )
345+ # process the authentication response
346+ binding = None if is_ecp else BINDING_HTTP_POST
347+ response = client .parse_authn_request_response (xmlstr , binding ,
348+ outstanding_queries )
281349 except (StatusError , ToEarly ):
282350 logger .exception ("Error processing SAML Assertion." )
283- return fail_acs_response (request )
351+ return fail_acs_response (request , soap = is_ecp )
284352 except ResponseLifetimeExceed :
285353 logger .info ("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack." , exc_info = True )
286- return fail_acs_response (request )
354+ return fail_acs_response (request , soap = is_ecp )
287355 except SignatureError :
288356 logger .info ("Invalid or malformed SAML Assertion." , exc_info = True )
289- return fail_acs_response (request )
357+ return fail_acs_response (request , soap = is_ecp )
290358 except StatusAuthnFailed :
291359 logger .info ("Authentication denied for user by IdP." , exc_info = True )
292- return fail_acs_response (request )
360+ return fail_acs_response (request , soap = is_ecp )
293361 except StatusRequestDenied :
294362 logger .warning ("Authentication interrupted at IdP." , exc_info = True )
295- return fail_acs_response (request )
363+ return fail_acs_response (request , soap = is_ecp )
296364 except MissingKey :
297365 logger .exception ("SAML Identity Provider is not configured correctly: certificate key is missing!" )
298- return fail_acs_response (request )
366+ return fail_acs_response (request , soap = is_ecp )
299367
300368 if response is None :
301369 logger .warning ("Invalid SAML Assertion received (unknown error)." )
302- return fail_acs_response (request , status = 400 , exc_class = SuspiciousOperation )
370+ return fail_acs_response (request , status = 400 ,
371+ exc_class = SuspiciousOperation , soap = is_ecp )
303372
304373 session_id = response .session_id ()
305374 oq_cache .delete (session_id )
@@ -318,6 +387,10 @@ def assertion_consumer_service(request,
318387 attribute_mapping = attribute_mapping ,
319388 create_unknown_user = create_unknown_user )
320389 if user is None :
390+ message = 'The user is None'
391+ logger .error (message )
392+ if is_ecp :
393+ return SoapFaultResponse (message , status = 403 )
321394 logger .warning ("Could not authenticate user received in SAML Assertion. Session info: %s" , session_info )
322395 raise PermissionDenied
323396
@@ -421,7 +494,7 @@ def logout_service_post(request, *args, **kwargs):
421494
422495
423496def do_logout_service (request , data , binding , config_loader_path = None , next_page = None ,
424- logout_error_template = 'djangosaml2/logout_error.html' ):
497+ logout_error_template = 'djangosaml2/logout_error.html' ):
425498 """SAML Logout Response endpoint
426499
427500 The IdP will send the logout response to this view,
@@ -509,4 +582,5 @@ def register_namespace_prefixes():
509582 for prefix , namespace in prefixes :
510583 ElementTree ._namespace_map [namespace ] = prefix
511584
585+
512586register_namespace_prefixes ()
0 commit comments