OpenID Connect RP support for customer accounts

This commit is contained in:
Raphael Michel
2022-07-11 12:45:51 +02:00
committed by Raphael Michel
parent e102a590ab
commit 7f5518dbf6
39 changed files with 1943 additions and 55 deletions

View File

@@ -39,6 +39,7 @@ from decimal import Decimal
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.signing import BadSignature, loads
from django.core.validators import EmailValidator
from django.db.models import F, Q
from django.http import HttpResponseNotAllowed, JsonResponse
@@ -261,7 +262,7 @@ class CustomerStep(CartMixin, TemplateFlowStep):
p.item.require_membership or
(p.variation and p.variation.require_membership)
for p in self.positions
)
) and self.request.event.settings.customer_accounts_native
@cached_property
def guest_allowed(self):
@@ -290,6 +291,22 @@ class CustomerStep(CartMixin, TemplateFlowStep):
field.widget.is_required = False
return f
def _handle_sso_login(self):
value = self.request.POST['login-sso-data']
try:
data = loads(value, salt=f'customer_sso_popup_{self.request.organizer.pk}', max_age=120)
except BadSignature:
return False
try:
customer = self.request.organizer.customers.get(pk=data['customer'], provider__isnull=False)
except Customer.DoesNotExist:
return False
self.cart_session['customer_mode'] = 'login'
self.cart_session['customer'] = customer.pk
self.cart_session['customer_cart_tied_to_login'] = True
customer_login(self.request, customer)
return True
def post(self, request):
self.request = request
@@ -301,7 +318,12 @@ class CustomerStep(CartMixin, TemplateFlowStep):
self.cart_session['customer'] = request.customer.pk
self.cart_session['customer_cart_tied_to_login'] = True
return redirect(self.get_next_url(request))
elif self.login_form.is_valid():
elif "login-sso-data" in self.request.POST:
if not self._handle_sso_login():
messages.error(request, _('We failed to process your authentication request, please try again.'))
return self.render()
return redirect(self.get_next_url(request))
elif self.event.settings.customer_accounts_native and self.login_form.is_valid():
customer_login(self.request, self.login_form.get_customer())
self.cart_session['customer_mode'] = 'login'
self.cart_session['customer'] = self.login_form.get_customer().pk
@@ -309,7 +331,7 @@ class CustomerStep(CartMixin, TemplateFlowStep):
return redirect(self.get_next_url(request))
else:
return self.render()
elif request.POST.get("customer_mode") == 'register':
elif request.POST.get("customer_mode") == 'register' and self.signup_allowed:
if self.register_form.is_valid():
customer = self.register_form.create()
self.cart_session['customer_mode'] = 'login'

View File

@@ -32,6 +32,7 @@ from django.contrib.auth.password_validation import (
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import signing
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import PhoneNumberField
@@ -83,7 +84,7 @@ class AuthenticationForm(forms.Form):
if email is not None and password:
try:
u = self.request.organizer.customers.get(email=email.lower())
u = self.request.organizer.customers.get(email=email.lower(), provider__isnull=True)
except Customer.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (django #20760).
@@ -333,7 +334,7 @@ class ResetPasswordForm(forms.Form):
if 'email' not in self.cleaned_data:
return
try:
self.customer = self.request.organizer.customers.get(email=self.cleaned_data['email'].lower())
self.customer = self.request.organizer.customers.get(email=self.cleaned_data['email'].lower(), provider__isnull=True)
return self.customer.email
except Customer.DoesNotExist:
# Yup, this is an information leak. But it prevents dozens of support requests and even if we didn't
@@ -473,6 +474,14 @@ class ChangeInfoForm(forms.ModelForm):
widget=WrappedPhoneNumberPrefixWidget()
)
if self.instance.provider_id is not None:
self.fields['email'].disabled = True
self.fields['email'].help_text = _(
'To change your email address, change it in your {provider} account and then log out and log in '
'again.'
).format(provider=escape(self.instance.provider.name))
del self.fields['password_current']
def clean_password_current(self):
old_pw = self.cleaned_data.get('password_current')
@@ -501,13 +510,13 @@ class ChangeInfoForm(forms.ModelForm):
email = self.cleaned_data.get('email')
password_current = self.cleaned_data.get('password_current')
if email != self.instance.email and not password_current:
if email != self.instance.email and not password_current and self.instance.provider_id is None:
raise forms.ValidationError(
self.error_messages['pw_current_wrong'],
code='pw_current_wrong',
)
if email is not None:
if email is not None and self.instance.provider_id is not None:
try:
self.request.organizer.customers.exclude(pk=self.instance.pk).get(email=email.lower())
except Customer.DoesNotExist:

View File

@@ -50,16 +50,31 @@
and access them at any time.
{% endblocktrans %}
</p>
{% bootstrap_form login_form layout="checkout" %}
{% if request.organizer.settings.customer_accounts_native %}
{% bootstrap_form login_form layout="checkout" %}
<div class="row">
<div class="col-md-offset-3 col-md-9">
<a
href="{% abseventurl request.organizer "presale:organizer.customer.resetpw" %}"
target="_blank">
{% trans "Reset password" %}
</a>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-offset-3 col-md-9">
<a
href="{% abseventurl request.organizer "presale:organizer.customer.resetpw" %}"
target="_blank">
{% trans "Reset password" %}
</a>
<div class="col-md-6 col-md-offset-3">
{% for provider in request.organizer.sso_providers.all %}
{% if provider.is_active %}
<a href="{% eventurl request.organizer "presale:organizer.customer.login" provider=provider.pk %}?next={% if request.event_domain %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.get_full_path|urlencode }}"
class="btn btn-primary btn-lg btn-block" data-open-in-popup-window>
{{ provider.button_label }}
</a>
{% endif %}
{% endfor %}
</div>
</div>
<input type="hidden" name="login-sso-data" id="login_sso_data">
{% endif %}
</div>
</div>

View File

@@ -14,6 +14,7 @@
<script type="text/javascript" src="{% static "pretixpresale/js/widget/floatformat.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/sso.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cookieconsent.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>

View File

@@ -4,6 +4,32 @@
{% load escapejson %}
<div id="ajaxerr">
</div>
<div id="popupmodal" hidden aria-live="polite">
<div class="modal-card">
<div class="modal-card-icon">
<i class="fa fa-window-restore big-icon" aria-hidden="true"></i>
</div>
<div class="modal-card-content">
<div>
<h3>
{% trans "We've started the requested process in a new window." %}
</h3>
<p class="text">
{% trans "If you do not see the new window, we can help you launch it again." %}
</p>
<p>
<a href="" data-open-in-popup-window class="btn btn-default">
<span class="fa fa-external-link-square"></span>
{% trans "Open window again" %}
</a>
</p>
<p class="text">
{% trans "Once the process in the new window has been completed, you can continue here." %}
</p>
</div>
</div>
</div>
</div>
<div id="loadingmodal" hidden aria-live="polite">
<div class="modal-card">
<div class="modal-card-icon">

View File

@@ -14,24 +14,38 @@
</h2>
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group buttons">
<button type="submit" class="btn btn-primary btn-lg btn-block">
{% trans "Log in" %}
</button>
</div>
<div class="row">
<div class="col-md-6">
<a class="btn btn-link btn-block" href="{% eventurl request.organizer "presale:organizer.customer.register" %}">
{% trans "Create account" %}
</a>
{% if request.organizer.settings.customer_accounts_native %}
{% bootstrap_form form %}
<div class="form-group buttons">
<button type="submit" class="btn btn-primary btn-lg btn-block">
{% trans "Log in" %}
</button>
</div>
<div class="col-md-6">
<a class="btn btn-link btn-block" href="{% eventurl request.organizer "presale:organizer.customer.resetpw" %}">
{% trans "Reset password" %}
{% endif %}
{% for provider in request.organizer.sso_providers.all %}
{% if provider.is_active %}
<a href="{% eventurl request.organizer "presale:organizer.customer.login" provider=provider.pk %}?{{ request.META.QUERY_STRING }}"
class="btn btn-primary btn-lg btn-block">
{{ provider.button_label }}
</a>
{% endif %}
{% endfor %}
{% if request.organizer.settings.customer_accounts_native %}
<div class="row">
<div class="col-md-6">
<a class="btn btn-link btn-block"
href="{% eventurl request.organizer "presale:organizer.customer.register" %}">
{% trans "Create account" %}
</a>
</div>
<div class="col-md-6">
<a class="btn btn-link btn-block"
href="{% eventurl request.organizer "presale:organizer.customer.resetpw" %}">
{% trans "Reset password" %}
</a>
</div>
</div>
</div>
{% endif %}
</form>
</div>
</div>

View File

@@ -19,14 +19,18 @@
<dl class="dl-horizontal">
<dt>{% trans "Customer ID" %}</dt>
<dd>#{{ customer.identifier }}</dd>
{% if customer.provider %}
<dt>{% trans "Login method" %}</dt>
<dd>{{ customer.provider.name }}</dd>
{% endif %}
<dt>{% trans "E-mail" %}</dt>
<dd>{{ customer.email }}
</dd>
<dt>{% trans "Name" %}</dt>
<dd>{{ customer.name }}</dd>
{% if customer.phone %}
<dt>{% trans "Phone" %}</dt>
<dd>{{ customer.phone }}</dd>
<dt>{% trans "Phone" %}</dt>
<dd>{{ customer.phone }}</dd>
{% endif %}
</dl>
<div class="text-right">
@@ -34,10 +38,12 @@
class="btn btn-default">
{% trans "Change account information" %}
</a>
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}"
class="btn btn-default">
{% trans "Change password" %}
</a>
{% if not customer.provider %}
<a href="{% eventurl request.organizer "presale:organizer.customer.password" %}"
class="btn btn-default">
{% trans "Change password" %}
</a>
{% endif %}
</div>
</div>
</div>

View File

@@ -0,0 +1,31 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixpresale/scss/waiting.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/postmessage.js" %}"></script>
{% endcompress %}
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="container">
<i class="fa fa-cog big-rotating-icon" aria-hidden="true"></i>
<h1>{% trans "We are processing your request …" %}</h1>
{{ message|json_script:"postmessage" }}
<script type="text/plain" id="origin">{{ origin }}</script>
<p>
{% trans "If this takes longer than a few minutes, please contact us." %}
</p>
</div>
</body>
</html>

View File

@@ -177,6 +177,8 @@ organizer_patterns = [
re_path(r'^widget/product_list$', pretix.presale.views.widget.WidgetAPIProductList.as_view(),
name='organizer.widget.productlist'),
re_path(r'^widget/v1.css$', pretix.presale.views.widget.widget_css, name='organizer.widget.css'),
re_path(r'^account/login/(?P<provider>[0-9]+)/$', pretix.presale.views.customer.SSOLoginView.as_view(), name='organizer.customer.login'),
re_path(r'^account/login/(?P<provider>[0-9]+)/return$', pretix.presale.views.customer.SSOLoginReturnView.as_view(), name='organizer.customer.login.return'),
re_path(r'^account/login$', pretix.presale.views.customer.LoginView.as_view(), name='organizer.customer.login'),
re_path(r'^account/logout$', pretix.presale.views.customer.LogoutView.as_view(), name='organizer.customer.logout'),
re_path(r'^account/register$', pretix.presale.views.customer.RegistrationView.as_view(), name='organizer.customer.register'),

View File

@@ -38,6 +38,7 @@ from urllib.parse import urljoin
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import Http404
from django.middleware.csrf import rotate_token
from django.shortcuts import redirect
@@ -87,6 +88,7 @@ def get_customer(request):
with scope(organizer=request.organizer):
try:
customer = request.organizer.customers.get(
Q(provider__isnull=True) | Q(provider__is_active=True),
is_active=True, is_verified=True,
pk=session[session_key]
)

View File

@@ -19,6 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import hashlib
from importlib import import_module
from urllib.parse import (
parse_qs, quote, urlencode, urljoin, urlparse, urlsplit, urlunparse,
@@ -26,11 +27,13 @@ from urllib.parse import (
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.signing import BadSignature, dumps, loads
from django.db import transaction
from django.db import IntegrityError, transaction
from django.db.models import Count, IntegerField, OuterRef, Q, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
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.http import url_has_allowed_host_and_scheme
@@ -40,8 +43,12 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import DeleteView, FormView, ListView, View
from pretix.base.customersso.oidc import (
oidc_authorize_url, oidc_validate_authorization,
)
from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition
from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.presale.forms.customer import (
@@ -58,9 +65,9 @@ SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
class RedirectBackMixin:
redirect_field_name = 'next'
def get_redirect_url(self):
def get_redirect_url(self, redirect_to=None):
"""Return the user-originating redirect URL if it's safe."""
redirect_to = self.request.POST.get(
redirect_to = redirect_to or self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
@@ -101,6 +108,11 @@ class LoginView(RedirectBackMixin, FormView):
return HttpResponseRedirect(redirect_to)
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts_native:
raise Http404('Feature not enabled')
return super().post(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['request'] = self.request
@@ -190,6 +202,8 @@ class RegistrationView(RedirectBackMixin, FormView):
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
raise Http404('Feature not enabled')
if not request.organizer.settings.customer_accounts_native:
raise Http404('Feature not enabled')
if self.redirect_authenticated_user and self.request.customer:
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
@@ -231,8 +245,10 @@ class SetPasswordView(FormView):
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
raise Http404('Feature not enabled')
if not request.organizer.settings.customer_accounts_native:
raise Http404('Feature not enabled')
try:
self.customer = request.organizer.customers.get(identifier=self.request.GET.get('id'))
self.customer = request.organizer.customers.get(identifier=self.request.GET.get('id'), provider__isnull=True)
except Customer.DoesNotExist:
messages.error(request, _('You clicked an invalid link.'))
return HttpResponseRedirect(self.get_success_url())
@@ -272,6 +288,8 @@ class ResetPasswordView(FormView):
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
raise Http404('Feature not enabled')
if not request.organizer.settings.customer_accounts_native:
raise Http404('Feature not enabled')
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
@@ -425,6 +443,8 @@ class ChangePasswordView(CustomerRequiredMixin, FormView):
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
raise Http404('Feature not enabled')
if self.request.customer.provider_id:
raise Http404('Feature not enabled')
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
@@ -464,7 +484,7 @@ class ChangeInformationView(CustomerRequiredMixin, FormView):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
def form_valid(self, form):
if form.cleaned_data['email'] != self.initial_email:
if form.cleaned_data['email'] != self.initial_email and not self.request.customer.provider:
new_email = form.cleaned_data['email']
form.cleaned_data['email'] = form.instance.email = self.initial_email
ctx = form.instance.get_email_context()
@@ -493,6 +513,7 @@ class ChangeInformationView(CustomerRequiredMixin, FormView):
with transaction.atomic():
form.save()
d = dict(form.cleaned_data)
print(d)
del d['email']
self.request.customer.log_action('pretix.customer.changed', d)
@@ -520,7 +541,7 @@ class ConfirmChangeView(View):
return HttpResponseRedirect(self.get_success_url())
try:
customer = request.organizer.customers.get(pk=data.get('customer'))
customer = request.organizer.customers.get(pk=data.get('customer'), provider__isnull=True)
except Customer.DoesNotExist:
messages.error(request, _('You clicked an invalid link.'))
return HttpResponseRedirect(self.get_success_url())
@@ -541,3 +562,286 @@ class ConfirmChangeView(View):
def get_success_url(self):
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
class SSOLoginView(RedirectBackMixin, View):
"""
Start logging in with a SSO provider.
"""
form_class = AuthenticationForm
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
raise Http404('Feature not enabled')
if self.redirect_authenticated_user and self.request.customer:
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check that "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
return super().dispatch(request, *args, **kwargs)
@cached_property
def provider(self):
return get_object_or_404(self.request.organizer.sso_providers.filter(is_active=True), pk=self.kwargs['provider'])
def get(self, request, *args, **kwargs):
next_url = request.GET.get('next') or ''
popup_origin = request.GET.get('popup_origin', '')
if popup_origin:
popup_origin_parsed = urlparse(popup_origin)
untrusted = (
popup_origin_parsed.hostname != urlparse(settings.SITE_URL).hostname and
not KnownDomain.objects.filter(domainname=popup_origin_parsed.hostname, organizer=self.request.organizer.pk).exists()
)
if untrusted:
# Do not accept faked origins
popup_origin = None
nonce = get_random_string(32)
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"
redirect_uri = build_absolute_uri(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
'provider': self.provider.pk
})
if self.provider.method == "oidc":
return redirect(oidc_authorize_url(self.provider, f'{nonce}#{next_url}', redirect_uri))
else:
raise Http404("Unknown SSO method.")
def get_success_url(self):
url = self.get_redirect_url()
if not url:
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
return url
class SSOLoginReturnView(RedirectBackMixin, View):
"""
Start logging in with a SSO provider.
"""
form_class = AuthenticationForm
redirect_authenticated_user = True
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if not request.organizer.settings.customer_accounts:
raise Http404('Feature not enabled')
if self.redirect_authenticated_user and self.request.customer:
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check that "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
r = super().dispatch(request, *args, **kwargs)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_nonce', None)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_popup_origin', None)
request.session.pop(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested', None)
return r
@cached_property
def provider(self):
return get_object_or_404(self.request.organizer.sso_providers.filter(is_active=True), pk=self.kwargs['provider'])
def get(self, request, *args, **kwargs):
redirect_to = None
popup_origin = None
if request.session.get(f'pretix_customerauth_{self.provider.pk}_popup_origin'):
popup_origin = request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin']
if self.provider.method == "oidc":
if not request.GET.get('state'):
return self._fail(
_('Login was not successful. Error message: "{error}".').format(
error='state parameter missing',
),
popup_origin,
)
nonce, redirect_to = request.GET['state'].split('#')
if nonce != request.session.get(f'pretix_customerauth_{self.provider.pk}_nonce'):
return self._fail(
_('Login was not successful. Error message: "{error}".').format(
error='invalid nonce',
),
popup_origin,
)
redirect_uri = build_absolute_uri(
self.request.organizer, 'presale:organizer.customer.login.return',
kwargs={
'provider': self.provider.pk
}
)
try:
profile = oidc_validate_authorization(
self.provider,
request.GET.get('code'),
redirect_uri,
)
except ValidationError as e:
for msg in e:
return self._fail(msg, popup_origin)
else:
raise Http404("Unknown SSO method.")
identifier = hashlib.sha256(
profile['uid'].encode() + b'@' + str(self.provider.pk).encode()
).hexdigest().upper()[:settings.ENTROPY['customer_identifier']]
if "1" not in identifier and "0" not in identifier:
# This is a hack to make sure the hash space does not overlap with the random identifiers generated by
# Customer.assign_identifier()
identifier = identifier[:4] + "1" + identifier[4:-1]
try:
customer = self.request.organizer.customers.get(
provider=self.provider,
identifier=identifier,
)
except Customer.MultipleObjectsReturned:
return self._fail(
_('Login was not successful. Error message: "{error}".').format(
error='identifier not unique',
),
popup_origin,
)
except Customer.DoesNotExist:
name_scheme = self.request.organizer.settings.name_scheme
name_parts = {
'_scheme': name_scheme,
}
scheme = PERSON_NAME_SCHEMES.get(name_scheme)
for fname, label, size in scheme['fields']:
if fname in profile:
name_parts[fname] = profile[fname]
if len(name_parts) == 1 and profile.get('name'):
name_parts = {'_legacy': profile['name']}
customer = Customer(
organizer=self.request.organizer,
identifier=identifier,
external_identifier=profile['uid'],
provider=self.provider,
email=profile['email'],
phone=profile.get('phone') or None,
name_parts=name_parts,
is_active=True,
is_verified=True, # todo: always?
locale=request.LANGUAGE_CODE,
)
try:
customer.save(force_insert=True)
except IntegrityError:
# This might either be a race condition or the email address is taken
# by a different customer account
try:
customer = self.request.organizer.customers.get(
provider=self.provider,
identifier=identifier,
)
except Customer.DoesNotExist:
return self._fail(
_('We were unable to use your login since the email address {email} is already used for a '
'different account in this system.').format(email=profile['email']),
popup_origin,
)
else:
if customer.is_active and customer.email != profile['email']:
customer.email = profile['email']
try:
customer.save(update_fields=['email'])
except IntegrityError:
return self._fail(
_('We were unable to use your login since the email address {email} is already used for a '
'different account in this system.').format(email=profile['email']),
popup_origin,
)
customer.log_action('pretix.customer.changed', {
'email': profile['email'],
'_source': 'provider'
})
if customer.external_identifier != profile['uid']:
return self._fail(
_('Login was not successful. Error message: "{error}".').format(
error='identifier not unique',
),
popup_origin,
)
if not customer.is_active:
self._fail(
AuthenticationForm.error_messages['inactive'],
popup_origin,
)
if not customer.is_verified:
return self._fail(
AuthenticationForm.error_messages['unverified'],
popup_origin
)
if popup_origin:
return render(self.request, 'pretixpresale/postmessage.html', {
'message': {
'__process': 'customer_sso_popup',
'status': 'ok',
'value': dumps({
'customer': customer.pk,
}, salt=f'customer_sso_popup_{self.request.organizer.pk}')
},
'origin': popup_origin,
})
else:
customer_login(self.request, customer)
return redirect(self.get_success_url(redirect_to))
def _fail(self, message, popup_origin):
if not popup_origin:
messages.error(
self.request,
message,
)
return redirect(eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={}))
else:
return render(self.request, 'pretixpresale/postmessage.html', {
'message': {
'__process': 'customer_sso_popup',
'status': 'error',
'value': str(message)
},
'origin': popup_origin,
})
def get_success_url(self, redirect_to=None):
url = self.get_redirect_url(redirect_to)
if not url:
return eventreverse(self.request.organizer, 'presale:organizer.customer.profile', kwargs={})
else:
if self.request.session.get(f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'):
otpstore = SessionStore()
otpstore[f'customer_cross_domain_auth_{self.request.organizer.pk}'] = self.request.session.session_key
otpstore.set_expiry(60)
otpstore.save(must_create=True)
otp = otpstore.session_key
u = urlparse(url)
qsl = parse_qs(u.query)
qsl['cross_domain_customer_auth'] = otp
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
return url