diff --git a/src/pretix/api/views/oauth.py b/src/pretix/api/views/oauth.py index 1a446fc94..3597df219 100644 --- a/src/pretix/api/views/oauth.py +++ b/src/pretix/api/views/oauth.py @@ -34,6 +34,7 @@ from oauth2_provider.views import ( from pretix.api.models import OAuthApplication from pretix.base.models import Organizer +from pretix.control.views.user import RecentAuthenticationRequiredMixin logger = logging.getLogger(__name__) @@ -54,7 +55,7 @@ class OAuthAllowForm(AllowForm): del self.fields['organizers'] -class AuthorizationView(BaseAuthorizationView): +class AuthorizationView(RecentAuthenticationRequiredMixin, BaseAuthorizationView): template_name = "pretixcontrol/auth/oauth_authorization.html" form_class = OAuthAllowForm diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index a973b92fb..68ac56eae 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -112,7 +112,7 @@ class PermissionMiddleware: url = resolve(request.path_info) url_name = url.url_name - if not request.path.startswith(get_script_prefix() + 'control'): + if not request.path.startswith(get_script_prefix() + 'control') and not (url.namespace.startswith("api-") and url_name == "authorize"): # This middleware should only touch the /control subpath return self.get_response(request) diff --git a/src/tests/api/test_auth.py b/src/tests/api/test_auth.py index ece50c0b2..a01f4cd99 100644 --- a/src/tests/api/test_auth.py +++ b/src/tests/api/test_auth.py @@ -19,6 +19,8 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import time + import pytest from pretix.base.models import Organizer @@ -48,6 +50,19 @@ def test_session_auth_with_teams(client, user, team): assert len(resp.data['results']) == 1 +@pytest.mark.django_db +def test_session_auth_relative_timeout(client, user, team): + client.login(email=user.email, password='dummy') + session = client.session + session['pretix_auth_long_session'] = False + session['pretix_auth_login_time'] = int(time.time()) - 3600 * 6 + session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60 + session.save() + + resp = client.get('/api/v1/organizers/') + assert resp.status_code == 403 + + @pytest.mark.django_db def test_token_invalid(client): client.credentials(HTTP_AUTHORIZATION='Token ABCDE') diff --git a/src/tests/api/test_oauth.py b/src/tests/api/test_oauth.py index 948c78de7..33e825c1c 100644 --- a/src/tests/api/test_oauth.py +++ b/src/tests/api/test_oauth.py @@ -34,6 +34,7 @@ import base64 import json +import time from urllib.parse import quote import pytest @@ -87,9 +88,81 @@ def test_authorize_require_login(client, application: OAuthApplication): assert resp['Location'].startswith('/control/login') +@pytest.mark.django_db +def test_authorize_require_login_after_absolute_timeout(client, admin_user, application: OAuthApplication): + client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_long_session'] = False + session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60 + session.save() + + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( + application.client_id, quote('https://example.org') + )) + assert resp.status_code == 302 + assert resp['Location'].startswith('/control/login') + + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( + application.client_id, quote(application.redirect_uris) + )) + assert resp.status_code == 302 + + +@pytest.mark.django_db +def test_authorize_require_recent_auth(client, admin_user, application: OAuthApplication): + client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_long_session'] = True + session['pretix_auth_login_time'] = int(time.time()) - 3600 - 60 + session['pretix_auth_last_used'] = int(time.time()) + session.save() + + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( + application.client_id, quote('https://example.org') + )) + assert resp.status_code == 302 + assert resp['Location'].startswith('/control/reauth') + + session['pretix_auth_login_time'] = int(time.time()) + session.save() + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( + application.client_id, quote(application.redirect_uris) + )) + assert resp.status_code == 302 + + +@pytest.mark.django_db +def test_authorize_require_login_after_relative_timeout(client, admin_user, application: OAuthApplication): + client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_long_session'] = False + session['pretix_auth_login_time'] = int(time.time()) - 3600 * 3 - 60 + session.save() + + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( + application.client_id, quote('https://example.org') + )) + assert resp.status_code == 302 + assert resp['Location'].startswith('/control/reauth') + + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() + resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( + application.client_id, quote(application.redirect_uris) + )) + assert resp.status_code == 302 + + @pytest.mark.django_db def test_authorize_invalid_redirect_uri(client, admin_user, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( application.client_id, quote('https://example.org') )) @@ -99,6 +172,9 @@ def test_authorize_invalid_redirect_uri(client, admin_user, application: OAuthAp @pytest.mark.django_db def test_authorize_missing_response_type(client, admin_user, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s' % ( application.client_id, quote(application.redirect_uris) )) @@ -109,6 +185,9 @@ def test_authorize_missing_response_type(client, admin_user, application: OAuthA @pytest.mark.django_db def test_authorize_require_organizer(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -128,6 +207,9 @@ def test_authorize_require_organizer(client, admin_user, organizer, application: @pytest.mark.django_db def test_authorize_denied(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -146,6 +228,9 @@ def test_authorize_denied(client, admin_user, organizer, application: OAuthAppli @pytest.mark.django_db def test_authorize_disallow_response_token(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=token' % ( application.client_id, quote(application.redirect_uris) )) @@ -156,6 +241,9 @@ def test_authorize_disallow_response_token(client, admin_user, organizer, applic @pytest.mark.django_db def test_authorize_read_scope(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -180,6 +268,9 @@ def test_authorize_read_scope(client, admin_user, organizer, application: OAuthA @pytest.mark.django_db def test_authorize_state(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=asdadf' % ( application.client_id, quote(application.redirect_uris) )) @@ -200,6 +291,9 @@ def test_authorize_state(client, admin_user, organizer, application: OAuthApplic @pytest.mark.django_db def test_authorize_default_scope(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -225,6 +319,9 @@ def test_authorize_default_scope(client, admin_user, organizer, application: OAu @pytest.mark.django_db def test_token_from_code_without_auth(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -252,6 +349,9 @@ def test_token_from_code_without_auth(client, admin_user, organizer, application @pytest.mark.django_db def test_token_from_code(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -291,6 +391,9 @@ def test_use_token_for_access_one_organizer(client, admin_user, organizer, appli t2.members.add(admin_user) client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -335,6 +438,9 @@ def test_use_token_for_access_two_organizers(client, admin_user, organizer, appl t2.members.add(admin_user) client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -376,6 +482,9 @@ def test_use_token_for_access_two_organizers(client, admin_user, organizer, appl @pytest.mark.django_db def test_token_refresh(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -418,6 +527,9 @@ def test_token_refresh(client, admin_user, organizer, application: OAuthApplicat @pytest.mark.django_db def test_allow_write(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -450,6 +562,9 @@ def test_allow_write(client, admin_user, organizer, application: OAuthApplicatio @pytest.mark.django_db def test_allow_read_only(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -482,6 +597,9 @@ def test_allow_read_only(client, admin_user, organizer, application: OAuthApplic @pytest.mark.django_db def test_token_revoke_refresh_token(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -526,6 +644,9 @@ def test_token_revoke_refresh_token(client, admin_user, organizer, application: @pytest.mark.django_db def test_token_revoke_access_token(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -574,6 +695,9 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O @pytest.mark.django_db def test_user_revoke(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % ( application.client_id, quote(application.redirect_uris) )) @@ -603,6 +727,9 @@ def test_user_revoke(client, admin_user, organizer, application: OAuthApplicatio at = OAuthAccessToken.objects.get(token=access_token) client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.post('/control/settings/oauth/authorized/{}/revoke'.format(at.pk), data={ }) assert resp.status_code == 302 @@ -621,6 +748,9 @@ def test_user_revoke(client, admin_user, organizer, application: OAuthApplicatio @pytest.mark.django_db def test_allow_profile_only(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=profile' % ( application.client_id, quote(application.redirect_uris) )) @@ -655,6 +785,9 @@ def test_allow_profile_only(client, admin_user, organizer, application: OAuthApp @pytest.mark.django_db def test_reject_other_response_types(client, admin_user, organizer, application: OAuthApplication): client.login(email='dummy@dummy.dummy', password='dummy') + session = client.session + session['pretix_auth_login_time'] = int(time.time()) + session.save() resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code+id_token' % ( application.client_id, quote(application.redirect_uris) )) diff --git a/src/tests/control/test_auth.py b/src/tests/control/test_auth.py index 0a29785f0..394bedea5 100644 --- a/src/tests/control/test_auth.py +++ b/src/tests/control/test_auth.py @@ -757,7 +757,7 @@ class SessionTimeOutTest(TestCase): # Regression test added after a security problem in 1.9.1 # The problem was that, once the relative timeout happened, the user was redirected # to /control/reauth/, but loading /control/reauth/ was already considered to be - # "session activitiy". Therefore, after loding /control/reauth/, the session was no longer + # "session activity". Therefore, after loding /control/reauth/, the session was no longer # in the timeout state and the user was able to access pages again without re-entering the # password. session = self.client.session