13
13
# See the License for the specific language governing permissions and
14
14
# limitations under the License.
15
15
16
+ import base64
16
17
import logging
17
18
18
19
try :
19
20
from xml .etree import ElementTree
20
21
except ImportError :
21
22
from elementtree import ElementTree
22
- from defusedxml .common import (DTDForbidden , EntitiesForbidden ,
23
- ExternalReferenceForbidden )
24
23
25
24
from django .conf import settings
26
25
from django .contrib import auth
@@ -50,13 +49,13 @@ def csrf_exempt(view_func):
50
49
from saml2 .sigver import MissingKey
51
50
from saml2 .s_utils import UnsupportedBinding
52
51
from saml2 .response import StatusError
52
+ from saml2 .xmldsig import SIG_RSA_SHA1 # support for this is required by spec
53
53
54
54
from djangosaml2 .cache import IdentityCache , OutstandingQueriesCache
55
55
from djangosaml2 .cache import StateCache
56
56
from djangosaml2 .conf import get_config
57
57
from djangosaml2 .signals import post_authenticated
58
- from djangosaml2 .utils import get_custom_setting , available_idps , get_location , \
59
- get_hidden_form_inputs , get_idp_sso_supported_bindings
58
+ from djangosaml2 .utils import get_custom_setting , available_idps , get_location , get_idp_sso_supported_bindings
60
59
61
60
62
61
logger = logging .getLogger ('djangosaml2' )
@@ -140,25 +139,10 @@ def login(request,
140
139
'came_from' : came_from ,
141
140
})
142
141
143
- # Choose binding (REDIRECT vs. POST).
144
- # When authn_requests_signed is turned on, HTTP Redirect binding cannot be
145
- # used the same way as without signatures; proper usage in this case involves
146
- # stripping out the signature from SAML XML message and creating a new
147
- # signature, following precise steps defined in the SAML2.0 standard.
148
- #
149
- # It is not feasible to implement this since we wouldn't be able to use an
150
- # external (xmlsec1) library to handle the signatures - more (higher level)
151
- # context is needed in order to create such signature (like the value of
152
- # RelayState parameter).
153
- #
154
- # Therefore it is much easier to use the HTTP POST binding in this case, as
155
- # it can relay the whole signed SAML message as is, without the need to
156
- # manipulate the signature or the XML message itself.
157
- #
158
- # Read more in the official SAML2 specs (3.4.4.1):
159
- # http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
142
+ # choose a binding to try first
160
143
sign_requests = getattr (conf , '_sp_authn_requests_signed' , False )
161
144
binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT
145
+ logger .debug ('Trying binding %s for IDP %s' , binding , selected_idp )
162
146
163
147
# ensure our selected binding is supported by the IDP
164
148
supported_bindings = get_idp_sso_supported_bindings (selected_idp , config = conf )
@@ -169,51 +153,70 @@ def login(request,
169
153
logger .warning ('IDP %s does not support %s, trying %s' ,
170
154
selected_idp , binding , BINDING_HTTP_REDIRECT )
171
155
binding = BINDING_HTTP_REDIRECT
172
- if sign_requests :
173
- sign_requests = False
174
- logger .warning ('sp_authn_requests_signed is True, but ignoring because pysaml2 does not support it for %s' , BINDING_HTTP_REDIRECT )
175
156
else :
157
+ logger .warning ('IDP %s does not support %s, trying %s' ,
158
+ selected_idp , binding , BINDING_HTTP_POST )
176
159
binding = BINDING_HTTP_POST
177
160
# if switched binding still not supported, give up
178
161
if binding not in supported_bindings :
179
- raise UnsupportedBinding ('IDP does not support %s or %s' ,
180
- BINDING_HTTP_POST , BINDING_HTTP_REDIRECT )
162
+ raise UnsupportedBinding ('IDP %s does not support %s or %s' ,
163
+ selected_idp , BINDING_HTTP_POST , BINDING_HTTP_REDIRECT )
181
164
182
165
client = Saml2Client (conf )
183
- try :
184
- # we use sign kwarg to override in case of redirect binding
185
- # otherwise pysaml2 may sign the xml for redirect which is incorrect
186
- (session_id , result ) = client .prepare_for_authenticate (
187
- entityid = selected_idp , relay_state = came_from ,
188
- binding = binding , sign = sign_requests ,
189
- )
190
- except TypeError as e :
191
- logger .error ('Unable to know which IdP to use' )
192
- return HttpResponse (text_type (e ))
193
-
194
- logger .debug ('Saving the session_id in the OutstandingQueries cache' )
195
- oq_cache = OutstandingQueriesCache (request .session )
196
- oq_cache .set (session_id , came_from )
166
+ http_response = None
197
167
198
- logger .debug ('Redirecting user to the IdP via %s binding.' , binding . split ( ':' )[ - 1 ] )
168
+ logger .debug ('Redirecting user to the IdP via %s binding.' , binding )
199
169
if binding == BINDING_HTTP_REDIRECT :
200
- return HttpResponseRedirect (get_location (result ))
170
+ try :
171
+ # do not sign the xml itself, instead us the sigalg to
172
+ # generate the signature as a URL param
173
+ sigalg = SIG_RSA_SHA1 if getattr (conf , '_sp_authn_requests_signed' , False ) else None
174
+ session_id , result = client .prepare_for_authenticate (
175
+ entityid = selected_idp , relay_state = came_from ,
176
+ binding = binding , sign = False , sigalg = sigalg )
177
+ except TypeError as e :
178
+ logger .error ('Unable to know which IdP to use' )
179
+ return HttpResponse (text_type (e ))
180
+ else :
181
+ http_response = HttpResponseRedirect (get_location (result ))
201
182
elif binding == BINDING_HTTP_POST :
183
+ # use the html provided by pysaml2 if no template specified
202
184
if not post_binding_form_template :
203
- return HttpResponse (result ['data' ])
204
- try :
205
- params = get_hidden_form_inputs (result ['data' ][3 ])
206
- return render (request , post_binding_form_template , {
207
- 'target_url' : result ['url' ],
208
- 'params' : params ,
209
- })
210
- except (DTDForbidden , EntitiesForbidden , ExternalReferenceForbidden ):
211
- raise PermissionDenied
212
- except TemplateDoesNotExist :
213
- return HttpResponse (result ['data' ])
185
+ try :
186
+ session_id , result = client .prepare_for_authenticate (
187
+ entityid = selected_idp , relay_state = came_from ,
188
+ binding = binding )
189
+ except TypeError as e :
190
+ logger .error ('Unable to know which IdP to use' )
191
+ return HttpResponse (text_type (e ))
192
+ else :
193
+ http_response = HttpResponse (result ['data' ])
194
+ # get request XML to build our own html based on the template
195
+ else :
196
+ try :
197
+ location = client .sso_location (selected_idp , binding )
198
+ except TypeError as e :
199
+ logger .error ('Unable to know which IdP to use' )
200
+ return HttpResponse (text_type (e ))
201
+ session_id , request_xml = client .create_authn_request (
202
+ location ,
203
+ binding = binding )
204
+ http_response = render (request , post_binding_form_template , {
205
+ 'target_url' : location ,
206
+ 'params' : {
207
+ 'SAMLRequest' : base64 .b64encode (request_xml ),
208
+ 'RelayState' : came_from ,
209
+ },
210
+ })
214
211
else :
215
212
raise UnsupportedBinding ('Unsupported binding: %s' , binding )
216
213
214
+ # success, so save the session ID and return our response
215
+ logger .debug ('Saving the session_id in the OutstandingQueries cache' )
216
+ oq_cache = OutstandingQueriesCache (request .session )
217
+ oq_cache .set (session_id , came_from )
218
+ return http_response
219
+
217
220
218
221
@require_POST
219
222
@csrf_exempt
@@ -278,7 +281,7 @@ def assertion_consumer_service(request,
278
281
create_unknown_user = create_unknown_user )
279
282
if user is None :
280
283
logger .error ('The user is None' )
281
- return render ( request , 'djangosaml2/permission_denied.html' , status = 403 )
284
+ raise PermissionDenied
282
285
283
286
auth .login (request , user )
284
287
_set_subject_id (request .session , session_info ['name_id' ])
0 commit comments