diff --git a/src/pretix/presale/templates/pretixpresale/fragment_login_status.html b/src/pretix/presale/templates/pretixpresale/fragment_login_status.html
index 5344e2242..faa164d6d 100644
--- a/src/pretix/presale/templates/pretixpresale/fragment_login_status.html
+++ b/src/pretix/presale/templates/pretixpresale/fragment_login_status.html
@@ -14,10 +14,23 @@
{% trans "Log out" %}
{% else %}
-
-
- {% trans "Log in" %}
-
+
{% endif %}
{% endif %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index 94df03d95..cab0c9050 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -98,6 +98,7 @@ event_patterns = [
re_path(r'unlock/(?P[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(),
name='event.payment.unlock'),
re_path(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
+ re_path(r'^account/loginstart$', pretix.presale.views.customer.LoginStartView.as_view(), name='event.customer.loginstart'),
re_path(r'^favicon.ico/?$',
pretix.presale.views.organizer.OrganizerFavicon.as_view(),
diff --git a/src/pretix/presale/utils.py b/src/pretix/presale/utils.py
index d0703a439..9242a3930 100644
--- a/src/pretix/presale/utils.py
+++ b/src/pretix/presale/utils.py
@@ -151,7 +151,9 @@ def add_customer_to_request(request):
else:
parent_session_key = otpstore.get(f'customer_cross_domain_auth_{request.organizer.pk}')
- if parent_session_key: # not already invalidated, expired, …
+ expected_nonce = request.session.pop('cross_domain_customer_auth_nonce', None)
+ found_nonce = request.GET.get("cross_domain_customer_auth_nonce")
+ if parent_session_key and expected_nonce and expected_nonce == found_nonce: # not already invalidated, expired, …
# Make sure the OTP can't be used again
otpstore.delete()
diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py
index eb2b65d3a..b1023cb90 100644
--- a/src/pretix/presale/views/customer.py
+++ b/src/pretix/presale/views/customer.py
@@ -147,6 +147,7 @@ class LoginView(RedirectBackMixin, FormView):
u = urlparse(url)
qsl = parse_qs(u.query)
qsl['cross_domain_customer_auth'] = otp
+ qsl['cross_domain_customer_auth_nonce'] = self.request.GET.get("request_cross_domain_customer_auth_nonce", "")
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
return url
@@ -705,6 +706,7 @@ class SSOLoginView(RedirectBackMixin, View):
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
+ request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_nonce'] = self.request.GET.get("request_cross_domain_customer_auth_nonce")
redirect_uri = eventreverse_absolute(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
'provider': self.provider.pk
})
@@ -952,6 +954,28 @@ class SSOLoginReturnView(RedirectBackMixin, View):
u = urlparse(url)
qsl = parse_qs(u.query)
qsl['cross_domain_customer_auth'] = otp
+ qsl['cross_domain_customer_auth_nonce'] = self.request.session.get(f'pretix_customerauth_{self.provider.pk}_cross_domain_nonce', '')
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
return url
+
+
+class LoginStartView(View):
+ # When a login is initiated on a event-domain-level view, we need to carry the user to the organizer domain through
+ # this POST request to be able to set a nonce on their current session. We can't just use a link, since then we'd
+ # need to create sessions for every anonymous user of the ticketshop, which is too expensive.
+
+ def post(self, request, *args, **kwargs):
+ if getattr(self.request, 'domain_mode', 'system') not in (KnownDomain.MODE_ORG_ALT_DOMAIN, KnownDomain.MODE_EVENT_DOMAIN):
+ raise Http404("Only active on event-level domains")
+
+ nonce = get_random_string(32)
+ request.session['cross_domain_customer_auth_nonce'] = nonce
+ query = {
+ "next": request.POST.get("next", ""),
+ "request_cross_domain_customer_auth_nonce": nonce,
+ "request_cross_domain_customer_auth": "true",
+ }
+ return redirect_to_url(
+ eventreverse_absolute(self.request.organizer, "presale:organizer.customer.login") + "?" + urlencode(query)
+ )
diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py
index b160b035d..0d712e187 100644
--- a/src/pretix/presale/views/order.py
+++ b/src/pretix/presale/views/order.py
@@ -42,7 +42,7 @@ import os
import re
from collections import Counter, OrderedDict, defaultdict
from decimal import Decimal
-from urllib.parse import quote
+from urllib.parse import quote, urlencode
from django import forms
from django.conf import settings
@@ -55,6 +55,7 @@ from django.http import (
FileResponse, Http404, HttpResponseRedirect, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -119,8 +120,14 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
login_url = eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
if hasattr(self.request, "event_domain") and self.request.event_domain:
- next_url = quote(self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path())
- return redirect_to_url(f'{login_url}?next={next_url}&request_cross_domain_customer_auth=true')
+ nonce = get_random_string(32)
+ self.request.session['cross_domain_customer_auth_nonce'] = nonce
+ query = {
+ "next": self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path(),
+ "request_cross_domain_customer_auth_nonce": nonce,
+ "request_cross_domain_customer_auth": "true",
+ }
+ return redirect_to_url(f'{login_url}?{urlencode(query)}')
else:
next_url = quote(self.request.get_full_path())
diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss
index 8b2e2a505..059130813 100644
--- a/src/pretix/static/pretixpresale/scss/main.scss
+++ b/src/pretix/static/pretixpresale/scss/main.scss
@@ -221,6 +221,11 @@ footer nav .btn-link {
/*border-bottom: 2px solid $brand-primary;*/
font-weight: bold;
}
+ .btn-link {
+ padding: 0;
+ margin-left: 5px;
+ vertical-align: top;
+ }
img {
vertical-align: baseline;
}
@@ -653,6 +658,35 @@ h2 .label {
}
+.helper-position-relative {
+ position: relative;
+}
+.helper-display-block {
+ display: block !important;
+}
+.helper-display-inline {
+ display: inline !important;
+}
+.helper-display-inline-block {
+ display: inline-block !important;
+}
+.helper-display-none-soft {
+ display: none;
+}
+.helper-display-none {
+ display: none !important;
+}
+.helper-width-auto {
+ width: auto;
+}
+.helper-width-100 {
+ width: 100%;
+}
+.helper-space-below {
+ margin-bottom: 10px;
+}
+
+
@import "_iframe.scss";
@import "_a11y.scss";
@import "_print.scss";
diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py
index dee2eabb4..506b1f480 100644
--- a/src/tests/presale/test_customer.py
+++ b/src/tests/presale/test_customer.py
@@ -721,9 +721,21 @@ def _cross_domain_login(env, client, client2, org_alt=False):
else:
KnownDomain.objects.create(domainname='event.test', organizer=env[0], event=env[1])
- # Log in on org domain
+ # Start session on event domain
path = '/conf/' if org_alt else '/'
- r = client.post(f'/account/login?next=https://event.test{path}redeem&request_cross_domain_customer_auth=true', {
+ r = client2.post(f'{path}account/loginstart', {
+ 'next': f'https://event.test{path}redeem',
+ }, HTTP_HOST='event.test')
+ assert r.status_code == 302
+ u = urlparse(r.headers['Location'])
+ assert u.netloc == 'org.test'
+ assert u.path == '/account/login'
+ assert 'request_cross_domain_customer_auth=' in u.query
+ assert 'request_cross_domain_customer_auth_nonce=' in u.query
+ assert 'next=' in u.query
+
+ # Log in on org domain
+ r = client.post(f'{u.path}?{u.query}', {
'email': 'john@example.org',
'password': 'foo',
}, HTTP_HOST='org.test')
@@ -734,6 +746,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
assert u.path == path + 'redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
+ assert 'cross_domain_customer_auth_nonce' in q
# Take session over to event domain
r = client2.get(f'{path}?{u.query}', HTTP_HOST='event.test')
@@ -745,7 +758,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
def test_cross_domain_login(env, client, client2):
_cross_domain_login(env, client, client2)
- # Logged in on evnet domain
+ # Logged in on event domain
r = client.get('/', HTTP_HOST='event.test')
assert r.status_code == 200
assert b'john@example.org' in r.content
@@ -896,7 +909,14 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
},
)
- url = f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true'
+ r = client2.post('/account/loginstart', {"next": "https://event.test/redeem"}, follow=False, HTTP_HOST='event.test')
+ assert r.status_code == 302
+ assert "/account/login" in r['Location']
+
+ u = urlparse(r.headers['Location'])
+ nonce = parse_qs(u.query)['request_cross_domain_customer_auth_nonce'][0]
+ url = (f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true&'
+ f'request_cross_domain_customer_auth_nonce={nonce}')
r = client.get(url, follow=False, HTTP_HOST='org.test')
assert r.status_code == 302
assert "/authorize" in r['Location']
@@ -910,6 +930,7 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
assert u.path == '/redeem'
q = parse_qs(u.query)
assert 'cross_domain_customer_auth' in q
+ assert 'cross_domain_customer_auth_nonce' in q
# Take session over to event domain
r = client2.get(f'/?{u.query}', HTTP_HOST='event.test')