Skip to content

Commit 9abd86f

Browse files
ReeceReece
authored andcommitted
Merge remote-tracking branch 'upstream/master' into liquidpele-patch-2
Conflicts: djangosaml2/views.py
2 parents ce05d8f + ed4661b commit 9abd86f

File tree

4 files changed

+59
-89
lines changed

4 files changed

+59
-89
lines changed

djangosaml2/templates/djangosaml2/permission_denied.html

Lines changed: 0 additions & 11 deletions
This file was deleted.

djangosaml2/utils.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from defusedxml import ElementTree
1615
from django.conf import settings
1716
from saml2.s_utils import UnknownSystemEntity
1817

1918

2019
def get_custom_setting(name, default=None):
21-
if hasattr(settings, name):
22-
return getattr(settings, name)
23-
else:
24-
return default
20+
return getattr(settings, name, default)
2521

2622

2723
def available_idps(config, langpref=None):
@@ -66,21 +62,3 @@ def get_location(http_info):
6662
header_name, header_value = headers[0]
6763
assert header_name == 'Location'
6864
return header_value
69-
70-
71-
def get_hidden_form_inputs(html):
72-
""" Extracts name/value pairs from hidden input tags in an html form."""
73-
pairs = dict()
74-
tree = ElementTree.fromstring(html.replace('&', '&'), forbid_dtd=True)
75-
# python 2.6 doesn't have iter
76-
if hasattr(tree, 'iter'):
77-
node_iter = tree.iter()
78-
else:
79-
node_iter = tree.getiterator()
80-
for node in node_iter:
81-
if node.tag == 'input':
82-
element = dict(node.items())
83-
if element['type'] == 'hidden':
84-
pairs[element['name']] = element['value']
85-
return pairs
86-

djangosaml2/views.py

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import base64
1617
import logging
1718

1819
try:
1920
from xml.etree import ElementTree
2021
except ImportError:
2122
from elementtree import ElementTree
22-
from defusedxml.common import (DTDForbidden, EntitiesForbidden,
23-
ExternalReferenceForbidden)
2423

2524
from django.conf import settings
2625
from django.contrib import auth
@@ -50,13 +49,13 @@ def csrf_exempt(view_func):
5049
from saml2.sigver import MissingKey
5150
from saml2.s_utils import UnsupportedBinding
5251
from saml2.response import StatusError
52+
from saml2.xmldsig import SIG_RSA_SHA1 # support for this is required by spec
5353

5454
from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
5555
from djangosaml2.cache import StateCache
5656
from djangosaml2.conf import get_config
5757
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
6059

6160

6261
logger = logging.getLogger('djangosaml2')
@@ -140,25 +139,10 @@ def login(request,
140139
'came_from': came_from,
141140
})
142141

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
160143
sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
161144
binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT
145+
logger.debug('Trying binding %s for IDP %s', binding, selected_idp)
162146

163147
# ensure our selected binding is supported by the IDP
164148
supported_bindings = get_idp_sso_supported_bindings(selected_idp, config=conf)
@@ -169,51 +153,70 @@ def login(request,
169153
logger.warning('IDP %s does not support %s, trying %s',
170154
selected_idp, binding, BINDING_HTTP_REDIRECT)
171155
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)
175156
else:
157+
logger.warning('IDP %s does not support %s, trying %s',
158+
selected_idp, binding, BINDING_HTTP_POST)
176159
binding = BINDING_HTTP_POST
177160
# if switched binding still not supported, give up
178161
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)
181164

182165
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
197167

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)
199169
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))
201182
elif binding == BINDING_HTTP_POST:
183+
# use the html provided by pysaml2 if no template specified
202184
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+
})
214211
else:
215212
raise UnsupportedBinding('Unsupported binding: %s', binding)
216213

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+
217220

218221
@require_POST
219222
@csrf_exempt
@@ -278,7 +281,7 @@ def assertion_consumer_service(request,
278281
create_unknown_user=create_unknown_user)
279282
if user is None:
280283
logger.error('The user is None')
281-
return render(request, 'djangosaml2/permission_denied.html', status=403)
284+
raise PermissionDenied
282285

283286
auth.login(request, user)
284287
_set_subject_id(request.session, session_info['name_id'])

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def read(*rnames):
5858
include_package_data=True,
5959
zip_safe=False,
6060
install_requires=[
61-
'pysaml2==4.0.5',
61+
'pysaml2==4.4.0',
6262
'defusedxml==0.4.1'
6363
],
6464
)

0 commit comments

Comments
 (0)