40
40
from django .utils .six import text_type , binary_type , PY3
41
41
from django .views .decorators .csrf import csrf_exempt
42
42
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
44
49
from saml2 .metadata import entity_descriptor
45
50
from saml2 .ident import code , decode
46
51
from saml2 .sigver import MissingKey
52
+ from saml2 .ecp_client import PAOS_HEADER_INFO
53
+ from saml2 .profile .ecp import RelayState
47
54
from saml2 .s_utils import UnsupportedBinding
48
55
from saml2 .response import StatusError , StatusAuthnFailed , SignatureError , StatusRequestDenied
49
56
from saml2 .validate import ResponseLifetimeExceed , ToEarly
52
59
from djangosaml2 .cache import IdentityCache , OutstandingQueriesCache
53
60
from djangosaml2 .cache import StateCache
54
61
from djangosaml2 .conf import get_config
55
- from djangosaml2 .overrides import Saml2Client
56
62
from djangosaml2 .signals import post_authenticated
57
63
from djangosaml2 .utils import (
58
64
available_idps , fail_acs_response , get_custom_setting ,
59
65
get_idp_sso_supported_bindings , get_location , is_safe_url_compat ,
66
+ XmlResponse , SoapFaultResponse
60
67
)
61
68
62
69
@@ -104,7 +111,13 @@ def login(request,
104
111
If set to None or nonexistent template, default form from the saml2 library
105
112
will be rendered.
106
113
"""
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' )
108
121
109
122
came_from = request .GET .get ('next' , settings .LOGIN_REDIRECT_URL )
110
123
if not came_from :
@@ -129,11 +142,15 @@ def login(request,
129
142
redirect_authenticated_user = getattr (settings , 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN' , True )
130
143
if redirect_authenticated_user :
131
144
return HttpResponseRedirect (came_from )
145
+ elif is_ecp :
146
+ return HttpResponse ()
132
147
else :
133
148
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
+ )
137
154
138
155
selected_idp = request .GET .get ('idp' , None )
139
156
conf = get_config (config_loader_path , request )
@@ -142,10 +159,14 @@ def login(request,
142
159
idps = available_idps (conf )
143
160
if selected_idp is None and len (idps ) > 1 :
144
161
logger .debug ('A discovery process is needed' )
145
- return render (request , wayf_template , {
162
+ return render (
163
+ request ,
164
+ wayf_template ,
165
+ {
146
166
'available_idps' : idps .items (),
147
167
'came_from' : came_from ,
148
- })
168
+ }
169
+ )
149
170
150
171
# choose a binding to try first
151
172
sign_requests = getattr (conf , '_sp_authn_requests_signed' , False )
@@ -171,9 +192,37 @@ def login(request,
171
192
selected_idp , BINDING_HTTP_POST , BINDING_HTTP_REDIRECT )
172
193
173
194
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 )
174
223
http_response = None
224
+ logger .debug ('Redirecting user to the IdP via %s binding.' , binding .split (':' )[- 1 ])
175
225
176
- logger .debug ('Redirecting user to the IdP via %s binding.' , binding )
177
226
if binding == BINDING_HTTP_REDIRECT :
178
227
try :
179
228
# do not sign the xml itself, instead use the sigalg to
@@ -252,45 +301,65 @@ def assertion_consumer_service(request,
252
301
djangosaml2.backends.Saml2Backend that should be
253
302
enabled in the settings.py
254
303
"""
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' )
264
311
312
+ conf = get_config (config_loader_path , request )
265
313
client = Saml2Client (conf , identity_cache = IdentityCache (request .session ))
266
314
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
+
267
332
oq_cache = OutstandingQueriesCache (request .session )
268
333
outstanding_queries = oq_cache .outstanding_queries ()
269
334
270
335
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 )
272
340
except (StatusError , ToEarly ):
273
341
logger .exception ("Error processing SAML Assertion." )
274
- return fail_acs_response (request )
342
+ return fail_acs_response (request , soap = is_ecp )
275
343
except ResponseLifetimeExceed :
276
344
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 )
278
346
except SignatureError :
279
347
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 )
281
349
except StatusAuthnFailed :
282
350
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 )
284
352
except StatusRequestDenied :
285
353
logger .warning ("Authentication interrupted at IdP." , exc_info = True )
286
- return fail_acs_response (request )
354
+ return fail_acs_response (request , soap = is_ecp )
287
355
except MissingKey :
288
356
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 )
290
358
291
359
if response is None :
292
360
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 )
294
363
295
364
session_id = response .session_id ()
296
365
oq_cache .delete (session_id )
@@ -309,6 +378,10 @@ def assertion_consumer_service(request,
309
378
attribute_mapping = attribute_mapping ,
310
379
create_unknown_user = create_unknown_user )
311
380
if user is None :
381
+ message = 'The user is None'
382
+ logger .error (message )
383
+ if is_ecp :
384
+ return SoapFaultResponse (message , status = 403 )
312
385
logger .warning ("Could not authenticate user received in SAML Assertion. Session info: %s" , session_info )
313
386
raise PermissionDenied
314
387
@@ -412,7 +485,7 @@ def logout_service_post(request, *args, **kwargs):
412
485
413
486
414
487
def 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' ):
416
489
"""SAML Logout Response endpoint
417
490
418
491
The IdP will send the logout response to this view,
@@ -500,4 +573,5 @@ def register_namespace_prefixes():
500
573
for prefix , namespace in prefixes :
501
574
ElementTree ._namespace_map [namespace ] = prefix
502
575
576
+
503
577
register_namespace_prefixes ()
0 commit comments