|
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
|
@@ -49,13 +48,13 @@ def csrf_exempt(view_func):
|
49 | 48 | from saml2.ident import code, decode
|
50 | 49 | from saml2.sigver import MissingKey
|
51 | 50 | from saml2.response import StatusError
|
| 51 | +from saml2.xmldsig import SIG_RSA_SHA1 # support for this is required by spec |
52 | 52 |
|
53 | 53 | from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
|
54 | 54 | from djangosaml2.cache import StateCache
|
55 | 55 | from djangosaml2.conf import get_config
|
56 | 56 | 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 |
59 | 58 |
|
60 | 59 |
|
61 | 60 | logger = logging.getLogger('djangosaml2')
|
@@ -139,58 +138,63 @@ def login(request,
|
139 | 138 | 'came_from': came_from,
|
140 | 139 | })
|
141 | 140 |
|
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 |
159 | 141 | binding = BINDING_HTTP_POST if getattr(conf, '_sp_authn_requests_signed', False) else BINDING_HTTP_REDIRECT
|
160 | 142 |
|
161 | 143 | 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 |
174 | 145 |
|
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) |
176 | 147 | 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)) |
178 | 160 | elif binding == BINDING_HTTP_POST:
|
| 161 | + # use the html provided by pysaml2 if no template specified |
179 | 162 | 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 | + }) |
191 | 189 | else:
|
192 | 190 | raise NotImplementedError('Unsupported binding: %s', binding)
|
193 | 191 |
|
| 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 | + |
194 | 198 |
|
195 | 199 | @require_POST
|
196 | 200 | @csrf_exempt
|
|
0 commit comments