Skip to content
Draft
4 changes: 4 additions & 0 deletions framework/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,10 @@ def validate_next_url(next_url):
:return: True if valid, False otherwise
"""

# allow redirection to angular locally
if settings.LOCAL_ANGULAR_URL in next_url and settings.DEBUG_MODE:
return True

# disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests
# like http:// or https:// depending on the use of SSL on the page already.
if next_url.startswith('//'):
Expand Down
18 changes: 18 additions & 0 deletions osf/migrations/0036_institution_sso_in_progress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.17 on 2026-02-16 14:34

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('osf', '0035_merge_20251215_1451'),
]

operations = [
migrations.AddField(
model_name='institution',
name='sso_in_progress',
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions osf/models/institution.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian
default='',
help_text='Full URL where institutional admins can access archived metrics reports.',
)
sso_in_progress = models.BooleanField(default=False)

class Meta:
# custom permissions for use in the OSF Admin App
Expand Down
27 changes: 27 additions & 0 deletions tests/test_auth_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,17 @@ def test_next_url_login_with_auth(self):
assert data.get('status_code') == http_status.HTTP_302_FOUND
assert data.get('next_url') == self.next_url

def test_next_url_angular_login_with_auth(self):
data = login_and_register_handler(self.auth, next_url=settings.LOCAL_ANGULAR_URL)
assert data.get('status_code') == http_status.HTTP_302_FOUND
assert data.get('next_url') == settings.LOCAL_ANGULAR_URL

def test_next_url_angular_login_without_auth(self):
request.url = web_url_for('auth_login', next=settings.LOCAL_ANGULAR_URL, _absolute=True)
data = login_and_register_handler(self.no_auth, next_url=settings.LOCAL_ANGULAR_URL)
assert data.get('status_code') == http_status.HTTP_302_FOUND
assert data.get('next_url') == cas.get_login_url(request.url)

def test_next_url_login_without_auth(self):
# login: user without auth
request.url = web_url_for('auth_login', next=self.next_url, _absolute=True)
Expand Down Expand Up @@ -827,6 +838,22 @@ def test_logout_with_no_parameter(self):
assert resp.status_code == http_status.HTTP_302_FOUND
assert cas.get_logout_url(self.goodbye_url) == resp.headers['Location']

@mock.patch('framework.auth.views.settings.LOCAL_ANGULAR_URL', 'http://localhost:4200')
def test_logout_with_angular_next_url_logged_in(self):
angular_url = 'http://localhost:4200/'
logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url)
resp = self.app.get(logout_url, auth=self.auth_user.auth)
assert resp.status_code == http_status.HTTP_302_FOUND
assert cas.get_logout_url(logout_url) == resp.headers['Location']

@mock.patch('framework.auth.views.settings.LOCAL_ANGULAR_URL', 'http://localhost:4200')
def test_logout_with_angular_next_url_logged_out(self):
angular_url = 'http://localhost:4200/'
logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url)
resp = self.app.get(logout_url, auth=None)
assert resp.status_code == http_status.HTTP_302_FOUND
assert angular_url == resp.headers['Location']


class TestResetPassword(OsfTestCase):

Expand Down
1 change: 1 addition & 0 deletions website/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def parent_dir(path):
INTERNAL_DOMAIN = DOMAIN
API_DOMAIN = PROTOCOL + 'localhost:8000/'
RESET_PASSWORD_URL = PROTOCOL + 'localhost:5000/resetpassword/' # TODO set angular reset password url
LOCAL_ANGULAR_URL = 'localhost:4200'

PREPRINT_PROVIDER_DOMAINS = {
'enabled': False,
Expand Down
Loading