Skip to content

Commit 3be1bbf

Browse files
author
Jaap Roes
authored
Fix #278: Allow ACS_DEFAULT_REDIRECT_URL to override LOGIN_REDIRECT_URL in more places (#285)
1 parent 0d184d2 commit 3be1bbf

File tree

4 files changed

+67
-17
lines changed

4 files changed

+67
-17
lines changed

djangosaml2/tests/__init__.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@
2727
from django.core.exceptions import ImproperlyConfigured
2828
from django.test import Client, TestCase, override_settings
2929
from django.test.client import RequestFactory
30-
from django.urls import reverse
30+
from django.urls import reverse, reverse_lazy
3131
from django.utils.encoding import force_text
3232
from djangosaml2 import views
3333
from djangosaml2.cache import OutstandingQueriesCache
3434
from djangosaml2.conf import get_config
3535
from djangosaml2.middleware import SamlSessionMiddleware
3636
from djangosaml2.tests import conf
37-
from djangosaml2.utils import (get_idp_sso_supported_bindings,
37+
from djangosaml2.utils import (get_fallback_login_redirect_url,
38+
get_idp_sso_supported_bindings,
3839
get_session_id_from_saml2,
3940
get_subject_id_from_saml2,
4041
saml2_from_httpredirect_request)
@@ -76,6 +77,23 @@ def test_get_config_missing_function(self):
7677
with self.assertRaisesMessage(ImproperlyConfigured, 'Module "djangosaml2.tests" does not define a "nonexisting_function" attribute/class'):
7778
get_config('djangosaml2.tests.nonexisting_function')
7879

80+
@override_settings(LOGIN_REDIRECT_URL='/accounts/profile/')
81+
def test_get_fallback_login_redirect_url(self):
82+
self.assertEqual(get_fallback_login_redirect_url(), '/accounts/profile/')
83+
84+
with override_settings():
85+
del settings.LOGIN_REDIRECT_URL
86+
# Neither LOGIN_REDIRECT_URL nor ACS_DEFAULT_REDIRECT_URL is configured
87+
self.assertEqual(get_fallback_login_redirect_url(), '/')
88+
89+
with override_settings(ACS_DEFAULT_REDIRECT_URL='testprofiles:dashboard'):
90+
# ACS_DEFAULT_REDIRECT_URL is configured, so it is used (and resolved)
91+
self.assertEqual(get_fallback_login_redirect_url(), '/dashboard/')
92+
93+
with override_settings(ACS_DEFAULT_REDIRECT_URL=reverse_lazy('testprofiles:dashboard')):
94+
# Lazy urls are resolved
95+
self.assertEqual(get_fallback_login_redirect_url(), '/dashboard/')
96+
7997

8098
class SAML2Tests(TestCase):
8199

@@ -158,7 +176,7 @@ def test_unsigned_post_authn_request(self):
158176
def test_login_evil_redirect(self):
159177
"""
160178
Make sure that if we give an URL other than our own host as the next
161-
parameter, it is replaced with the default LOGIN_REDIRECT_URL.
179+
parameter, it is replaced with the fallback login redirect url.
162180
"""
163181

164182
# monkey patch SAML configuration
@@ -178,10 +196,19 @@ def test_login_evil_redirect(self):
178196

179197
self.assertEqual(params['RelayState'], ['/dashboard/'])
180198

199+
with self.subTest(ACS_DEFAULT_REDIRECT_URL=redirect_url):
200+
with override_settings(ACS_DEFAULT_REDIRECT_URL=redirect_url):
201+
response = self.client.get(
202+
reverse('saml2_login') + '?next=http://evil.com')
203+
url = urlparse(response['Location'])
204+
params = parse_qs(url.query)
205+
206+
self.assertEqual(params['RelayState'], ['/dashboard/'])
207+
181208
def test_no_redirect(self):
182209
"""
183210
Make sure that if we give an empty path as the next parameter,
184-
it is replaced with the default LOGIN_REDIRECT_URL.
211+
it is replaced with the fallback login redirect url.
185212
"""
186213

187214
# monkey patch SAML configuration
@@ -200,6 +227,14 @@ def test_no_redirect(self):
200227

201228
self.assertEqual(params['RelayState'], ['/dashboard/'])
202229

230+
with self.subTest(ACS_DEFAULT_REDIRECT_URL=redirect_url):
231+
with override_settings(ACS_DEFAULT_REDIRECT_URL=redirect_url):
232+
response = self.client.get(reverse('saml2_login') + '?next=')
233+
url = urlparse(response['Location'])
234+
params = parse_qs(url.query)
235+
236+
self.assertEqual(params['RelayState'], ['/dashboard/'])
237+
203238
@override_settings(SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN=True)
204239
def test_login_already_logged(self):
205240
self.client.force_login(User.objects.create(username='user', password='pass'))
@@ -215,6 +250,16 @@ def test_login_already_logged(self):
215250
response = self.client.get(reverse('saml2_login') + '?next=http://evil.com')
216251
self.assertRedirects(response, '/dashboard/')
217252

253+
with self.subTest(ACS_DEFAULT_REDIRECT_URL=redirect_url):
254+
with override_settings(ACS_DEFAULT_REDIRECT_URL=redirect_url):
255+
with self.subTest('no next url'):
256+
response = self.client.get(reverse('saml2_login'))
257+
self.assertRedirects(response, '/dashboard/')
258+
259+
with self.subTest('evil next url'):
260+
response = self.client.get(reverse('saml2_login') + '?next=http://evil.com')
261+
self.assertRedirects(response, '/dashboard/')
262+
218263
def test_unknown_idp(self):
219264
# monkey patch SAML configuration
220265
settings.SAML_CONFIG = conf.create_conf(
@@ -300,7 +345,7 @@ def test_login_several_idps(self):
300345
self.assertIn('AuthnRequest xmlns', decode_base64_and_inflate(
301346
saml_request).decode('utf-8'))
302347

303-
@override_settings(LOGIN_REDIRECT_URL='testprofiles:dashboard')
348+
@override_settings(ACS_DEFAULT_REDIRECT_URL='testprofiles:dashboard')
304349
def test_assertion_consumer_service(self):
305350
# Get initial number of users
306351
initial_user_count = User.objects.count()
@@ -350,11 +395,11 @@ def test_assertion_consumer_service(self):
350395
'RelayState': came_from,
351396
})
352397

353-
# as the RelayState is empty we have redirect to LOGIN_REDIRECT_URL
398+
# as the RelayState is empty we have redirect to ACS_DEFAULT_REDIRECT_URL
354399
self.assertRedirects(response, '/dashboard/')
355400
self.assertEqual(force_text(new_user.id), client.session[SESSION_KEY])
356401

357-
@override_settings(LOGIN_REDIRECT_URL='testprofiles:dashboard')
402+
@override_settings(ACS_DEFAULT_REDIRECT_URL='testprofiles:dashboard')
358403
def test_assertion_consumer_service_default_relay_state(self):
359404
settings.SAML_CONFIG = conf.create_conf(
360405
sp_host='sp.example.com',
@@ -375,7 +420,7 @@ def test_assertion_consumer_service_default_relay_state(self):
375420
})
376421
self.assertEqual(response.status_code, 302)
377422

378-
# The RelayState is missing, redirect to LOGIN_REDIRECT_URL
423+
# The RelayState is missing, redirect to ACS_DEFAULT_REDIRECT_URL
379424
self.assertRedirects(response, '/dashboard/')
380425
self.assertEqual(force_text(new_user.id), self.client.session[SESSION_KEY])
381426

djangosaml2/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ def get_location(http_info):
8282
return http_info['url']
8383

8484

85+
def get_fallback_login_redirect_url():
86+
login_redirect_url = get_custom_setting('LOGIN_REDIRECT_URL', '/')
87+
return resolve_url(get_custom_setting('ACS_DEFAULT_REDIRECT_URL', login_redirect_url))
88+
89+
8590
def validate_referral_url(request, url):
8691
# Ensure the user-originating redirection url is safe.
8792
# By setting SAML_ALLOWED_HOSTS in settings.py the user may provide a list of "allowed"
@@ -92,7 +97,7 @@ def validate_referral_url(request, url):
9297
getattr(settings, 'SAML_ALLOWED_HOSTS', [request.get_host()]))
9398

9499
if not is_safe_url(url=url, allowed_hosts=saml_allowed_hosts):
95-
return resolve_url(settings.LOGIN_REDIRECT_URL)
100+
return get_fallback_login_redirect_url()
96101
return url
97102

98103

djangosaml2/views.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from django.core.exceptions import PermissionDenied, SuspiciousOperation
2525
from django.http import (HttpRequest, HttpResponse, HttpResponseBadRequest,
2626
HttpResponseRedirect, HttpResponseServerError)
27-
from django.shortcuts import render, resolve_url
27+
from django.shortcuts import render
2828
from django.template import TemplateDoesNotExist
2929
from django.urls import reverse
3030
from django.utils.decorators import method_decorator
@@ -52,6 +52,7 @@
5252
from .exceptions import IdPConfigurationMissing
5353
from .overrides import Saml2Client
5454
from .utils import (add_idp_hinting, available_idps, get_custom_setting,
55+
get_fallback_login_redirect_url,
5556
get_idp_sso_supported_bindings, get_location,
5657
validate_referral_url)
5758

@@ -116,7 +117,7 @@ def get_next_path(self, request: HttpRequest) -> str:
116117
If the user is already logged in (and if allowed), he will redirect to there immediately.
117118
'''
118119

119-
next_path = resolve_url(settings.LOGIN_REDIRECT_URL)
120+
next_path = get_fallback_login_redirect_url()
120121
if 'next' in request.GET:
121122
next_path = request.GET['next']
122123
elif 'RelayState' in request.GET:
@@ -500,9 +501,7 @@ def post_login_hook(self, request: HttpRequest, user: settings.AUTH_USER_MODEL,
500501
def build_relay_state(self) -> str:
501502
""" The relay state is a URL used to redirect the user to the view where they came from.
502503
"""
503-
login_redirect_url = get_custom_setting('LOGIN_REDIRECT_URL', '/')
504-
default_relay_state = resolve_url(
505-
get_custom_setting('ACS_DEFAULT_REDIRECT_URL', login_redirect_url))
504+
default_relay_state = get_fallback_login_redirect_url()
506505
relay_state = self.request.POST.get('RelayState', default_relay_state)
507506
relay_state = self.customize_relay_state(relay_state)
508507
if not relay_state:

docs/source/contents/setup.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ hostnames to be used for the post-login redirect. In such cases, the setting::
138138
May be set to a list of allowed post-login redirect hostnames (note, the URL components beyond the hostname
139139
may be specified by the client - typically with the ?next= parameter.)
140140

141-
In the absence of a `?next=parameter`, the `LOGIN_REDIRECT_URL` setting will be used (assuming the destination hostname
142-
either matches the output of get_host() or is included in the SAML_ALLOWED_HOSTS setting)
141+
In the absence of a ``?next=parameter``, the ``ACS_DEFAULT_REDIRECT_URL`` or ``LOGIN_REDIRECT_URL`` setting will
142+
be used (assuming the destination hostname either matches the output of get_host() or is included in the
143+
``SAML_ALLOWED_HOSTS`` setting)
143144

144145
Preferred sso binding
145146
=====================
@@ -312,7 +313,7 @@ authentication::
312313

313314
Particularly useful when you only plan to use
314315
IdP initiated login and the IdP does not have a configured RelayState
315-
parameter. The default is ``/``.
316+
parameter. If not set Django's ``LOGIN_REDIRECT_URL`` or ``/`` will be used.
316317

317318
The other thing you will probably want to configure is the mapping of
318319
SAML2 user attributes to Django user attributes. By default only the

0 commit comments

Comments
 (0)