Skip to content

Commit ed4661b

Browse files
authored
Merge pull request #54 from liquidpele/liquidpele-patch-3
Support Redirect binding with signed AuthnRequests
2 parents 4c96ab1 + b6a428c commit ed4661b

File tree

2 files changed

+51
-69
lines changed

2 files changed

+51
-69
lines changed

djangosaml2/utils.py

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,11 @@
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

1817

1918
def get_custom_setting(name, default=None):
20-
if hasattr(settings, name):
21-
return getattr(settings, name)
22-
else:
23-
return default
19+
return getattr(settings, name, default)
2420

2521

2622
def available_idps(config, langpref=None):
@@ -46,21 +42,3 @@ def get_location(http_info):
4642
header_name, header_value = headers[0]
4743
assert header_name == 'Location'
4844
return header_value
49-
50-
51-
def get_hidden_form_inputs(html):
52-
""" Extracts name/value pairs from hidden input tags in an html form."""
53-
pairs = dict()
54-
tree = ElementTree.fromstring(html.replace('&', '&'), forbid_dtd=True)
55-
# python 2.6 doesn't have iter
56-
if hasattr(tree, 'iter'):
57-
node_iter = tree.iter()
58-
else:
59-
node_iter = tree.getiterator()
60-
for node in node_iter:
61-
if node.tag == 'input':
62-
element = dict(node.items())
63-
if element['type'] == 'hidden':
64-
pairs[element['name']] = element['value']
65-
return pairs
66-

djangosaml2/views.py

Lines changed: 50 additions & 46 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
@@ -49,13 +48,13 @@ def csrf_exempt(view_func):
4948
from saml2.ident import code, decode
5049
from saml2.sigver import MissingKey
5150
from saml2.response import StatusError
51+
from saml2.xmldsig import SIG_RSA_SHA1 # support for this is required by spec
5252

5353
from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
5454
from djangosaml2.cache import StateCache
5555
from djangosaml2.conf import get_config
5656
from djangosaml2.signals import post_authenticated
57-
from djangosaml2.utils import get_custom_setting, available_idps, get_location, \
58-
get_hidden_form_inputs
57+
from djangosaml2.utils import get_custom_setting, available_idps, get_location
5958

6059

6160
logger = logging.getLogger('djangosaml2')
@@ -139,58 +138,63 @@ def login(request,
139138
'came_from': came_from,
140139
})
141140

142-
# Choose binding (REDIRECT vs. POST).
143-
# When authn_requests_signed is turned on, HTTP Redirect binding cannot be
144-
# used the same way as without signatures; proper usage in this case involves
145-
# stripping out the signature from SAML XML message and creating a new
146-
# signature, following precise steps defined in the SAML2.0 standard.
147-
#
148-
# It is not feasible to implement this since we wouldn't be able to use an
149-
# external (xmlsec1) library to handle the signatures - more (higher level)
150-
# context is needed in order to create such signature (like the value of
151-
# RelayState parameter).
152-
#
153-
# Therefore it is much easier to use the HTTP POST binding in this case, as
154-
# it can relay the whole signed SAML message as is, without the need to
155-
# manipulate the signature or the XML message itself.
156-
#
157-
# Read more in the official SAML2 specs (3.4.4.1):
158-
# http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
159141
binding = BINDING_HTTP_POST if getattr(conf, '_sp_authn_requests_signed', False) else BINDING_HTTP_REDIRECT
160142

161143
client = Saml2Client(conf)
162-
try:
163-
(session_id, result) = client.prepare_for_authenticate(
164-
entityid=selected_idp, relay_state=came_from,
165-
binding=binding,
166-
)
167-
except TypeError as e:
168-
logger.error('Unable to know which IdP to use')
169-
return HttpResponse(text_type(e))
170-
171-
logger.debug('Saving the session_id in the OutstandingQueries cache')
172-
oq_cache = OutstandingQueriesCache(request.session)
173-
oq_cache.set(session_id, came_from)
144+
http_response = None
174145

175-
logger.debug('Redirecting user to the IdP via %s binding.', binding.split(':')[-1])
146+
logger.debug('Redirecting user to the IdP via %s binding.', binding)
176147
if binding == BINDING_HTTP_REDIRECT:
177-
return HttpResponseRedirect(get_location(result))
148+
try:
149+
# do not sign the xml itself, instead us the sigalg to
150+
# generate the signature as a URL param
151+
sigalg = SIG_RSA_SHA1 if getattr(conf, '_sp_authn_requests_signed', False) else None
152+
session_id, result = client.prepare_for_authenticate(
153+
entityid=selected_idp, relay_state=came_from,
154+
binding=binding, sign=False, sigalg=sigalg)
155+
except TypeError as e:
156+
logger.error('Unable to know which IdP to use')
157+
return HttpResponse(text_type(e))
158+
else:
159+
http_response = HttpResponseRedirect(get_location(result))
178160
elif binding == BINDING_HTTP_POST:
161+
# use the html provided by pysaml2 if no template specified
179162
if not post_binding_form_template:
180-
return HttpResponse(result['data'])
181-
try:
182-
params = get_hidden_form_inputs(result['data'][3])
183-
return render(request, post_binding_form_template, {
184-
'target_url': result['url'],
185-
'params': params,
186-
})
187-
except (DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden):
188-
raise PermissionDenied
189-
except TemplateDoesNotExist:
190-
return HttpResponse(result['data'])
163+
try:
164+
session_id, result = client.prepare_for_authenticate(
165+
entityid=selected_idp, relay_state=came_from,
166+
binding=binding)
167+
except TypeError as e:
168+
logger.error('Unable to know which IdP to use')
169+
return HttpResponse(text_type(e))
170+
else:
171+
http_response = HttpResponse(result['data'])
172+
# get request XML to build our own html based on the template
173+
else:
174+
try:
175+
location = client.sso_location(selected_idp, binding)
176+
except TypeError as e:
177+
logger.error('Unable to know which IdP to use')
178+
return HttpResponse(text_type(e))
179+
session_id, request_xml = client.create_authn_request(
180+
location,
181+
binding=binding)
182+
http_response = render(request, post_binding_form_template, {
183+
'target_url': location,
184+
'params': {
185+
'SAMLRequest': base64.b64encode(request_xml),
186+
'RelayState': came_from,
187+
},
188+
})
191189
else:
192190
raise NotImplementedError('Unsupported binding: %s', binding)
193191

192+
# success, so save the session ID and return our response
193+
logger.debug('Saving the session_id in the OutstandingQueries cache')
194+
oq_cache = OutstandingQueriesCache(request.session)
195+
oq_cache.set(session_id, came_from)
196+
return http_response
197+
194198

195199
@require_POST
196200
@csrf_exempt

0 commit comments

Comments
 (0)