49
49
from django .utils .six import text_type , binary_type , PY3
50
50
from django .views .decorators .csrf import csrf_exempt
51
51
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
53
58
from saml2 .metadata import entity_descriptor
54
59
from saml2 .ident import code , decode
55
60
from saml2 .sigver import MissingKey
61
+ from saml2 .ecp_client import PAOS_HEADER_INFO
62
+ from saml2 .profile .ecp import RelayState
56
63
from saml2 .s_utils import UnsupportedBinding
57
64
from saml2 .response import StatusError , StatusAuthnFailed , SignatureError , StatusRequestDenied
58
65
from saml2 .validate import ResponseLifetimeExceed , ToEarly
61
68
from djangosaml2 .cache import IdentityCache , OutstandingQueriesCache
62
69
from djangosaml2 .cache import StateCache
63
70
from djangosaml2 .conf import get_config
64
- from djangosaml2 .overrides import Saml2Client
65
71
from djangosaml2 .signals import post_authenticated
66
72
from djangosaml2 .utils import (
67
73
available_idps , fail_acs_response , get_custom_setting ,
68
74
get_idp_sso_supported_bindings , get_location , is_safe_url_compat ,
75
+ XmlResponse , SoapFaultResponse
69
76
)
70
77
71
78
@@ -113,7 +120,13 @@ def login(request,
113
120
If set to None or nonexistent template, default form from the saml2 library
114
121
will be rendered.
115
122
"""
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' )
117
130
118
131
came_from = request .GET .get ('next' , settings .LOGIN_REDIRECT_URL )
119
132
if not came_from :
@@ -138,11 +151,15 @@ def login(request,
138
151
redirect_authenticated_user = getattr (settings , 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN' , True )
139
152
if redirect_authenticated_user :
140
153
return HttpResponseRedirect (came_from )
154
+ elif is_ecp :
155
+ return HttpResponse ()
141
156
else :
142
157
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
+ )
146
163
147
164
selected_idp = request .GET .get ('idp' , None )
148
165
conf = get_config (config_loader_path , request )
@@ -151,10 +168,14 @@ def login(request,
151
168
idps = available_idps (conf )
152
169
if selected_idp is None and len (idps ) > 1 :
153
170
logger .debug ('A discovery process is needed' )
154
- return render (request , wayf_template , {
171
+ return render (
172
+ request ,
173
+ wayf_template ,
174
+ {
155
175
'available_idps' : idps .items (),
156
176
'came_from' : came_from ,
157
- })
177
+ }
178
+ )
158
179
159
180
# choose a binding to try first
160
181
sign_requests = getattr (conf , '_sp_authn_requests_signed' , False )
@@ -180,9 +201,37 @@ def login(request,
180
201
selected_idp , BINDING_HTTP_POST , BINDING_HTTP_REDIRECT )
181
202
182
203
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 )
183
232
http_response = None
233
+ logger .debug ('Redirecting user to the IdP via %s binding.' , binding .split (':' )[- 1 ])
184
234
185
- logger .debug ('Redirecting user to the IdP via %s binding.' , binding )
186
235
if binding == BINDING_HTTP_REDIRECT :
187
236
try :
188
237
# do not sign the xml itself, instead use the sigalg to
@@ -261,45 +310,65 @@ def assertion_consumer_service(request,
261
310
djangosaml2.backends.Saml2Backend that should be
262
311
enabled in the settings.py
263
312
"""
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' )
273
320
321
+ conf = get_config (config_loader_path , request )
274
322
client = Saml2Client (conf , identity_cache = IdentityCache (request .session ))
275
323
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
+
276
341
oq_cache = OutstandingQueriesCache (request .session )
277
342
outstanding_queries = oq_cache .outstanding_queries ()
278
343
279
344
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 )
281
349
except (StatusError , ToEarly ):
282
350
logger .exception ("Error processing SAML Assertion." )
283
- return fail_acs_response (request )
351
+ return fail_acs_response (request , soap = is_ecp )
284
352
except ResponseLifetimeExceed :
285
353
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 )
287
355
except SignatureError :
288
356
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 )
290
358
except StatusAuthnFailed :
291
359
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 )
293
361
except StatusRequestDenied :
294
362
logger .warning ("Authentication interrupted at IdP." , exc_info = True )
295
- return fail_acs_response (request )
363
+ return fail_acs_response (request , soap = is_ecp )
296
364
except MissingKey :
297
365
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 )
299
367
300
368
if response is None :
301
369
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 )
303
372
304
373
session_id = response .session_id ()
305
374
oq_cache .delete (session_id )
@@ -318,6 +387,10 @@ def assertion_consumer_service(request,
318
387
attribute_mapping = attribute_mapping ,
319
388
create_unknown_user = create_unknown_user )
320
389
if user is None :
390
+ message = 'The user is None'
391
+ logger .error (message )
392
+ if is_ecp :
393
+ return SoapFaultResponse (message , status = 403 )
321
394
logger .warning ("Could not authenticate user received in SAML Assertion. Session info: %s" , session_info )
322
395
raise PermissionDenied
323
396
@@ -421,7 +494,7 @@ def logout_service_post(request, *args, **kwargs):
421
494
422
495
423
496
def 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' ):
425
498
"""SAML Logout Response endpoint
426
499
427
500
The IdP will send the logout response to this view,
@@ -509,4 +582,5 @@ def register_namespace_prefixes():
509
582
for prefix , namespace in prefixes :
510
583
ElementTree ._namespace_map [namespace ] = prefix
511
584
585
+
512
586
register_namespace_prefixes ()
0 commit comments