diff --git a/src/pretix/control/templates/pretixcontrol/auth/login.html b/src/pretix/control/templates/pretixcontrol/auth/login.html
index 64e049e67e..20eb0007c8 100644
--- a/src/pretix/control/templates/pretixcontrol/auth/login.html
+++ b/src/pretix/control/templates/pretixcontrol/auth/login.html
@@ -55,7 +55,8 @@
{% if login_providers %}
{% for provider, settings in login_providers.items %}
{% if settings.state %}
-
+
{% with provider|capfirst as provider_capitalized %}
{% blocktrans %}Login with {{ provider_capitalized }}{% endblocktrans %}
{% endwith %}
diff --git a/src/pretix/plugins/socialauth/schemas/oauth2_params.py b/src/pretix/plugins/socialauth/schemas/oauth2_params.py
new file mode 100644
index 0000000000..96ac44d8dd
--- /dev/null
+++ b/src/pretix/plugins/socialauth/schemas/oauth2_params.py
@@ -0,0 +1,11 @@
+from typing import Annotated
+
+from pydantic import BaseModel, StringConstraints
+
+
+class OAuth2Params(BaseModel):
+ response_type: Annotated[str, StringConstraints(strip_whitespace=True)] = "code"
+ client_id: Annotated[str, StringConstraints(strip_whitespace=True)]
+ redirect_uri: Annotated[str, StringConstraints(strip_whitespace=True)]
+ scope: Annotated[str, StringConstraints(strip_whitespace=True)] = "profile"
+ state: Annotated[str, StringConstraints(strip_whitespace=True)]
diff --git a/src/pretix/plugins/socialauth/urls.py b/src/pretix/plugins/socialauth/urls.py
index 7f8b593e1b..2c5d590e5f 100644
--- a/src/pretix/plugins/socialauth/urls.py
+++ b/src/pretix/plugins/socialauth/urls.py
@@ -3,7 +3,7 @@
from . import views
urlpatterns = [
- path('oauth_login//', views.oauth_login, name='social.oauth.login'),
- path('oauth_return/', views.oauth_return, name='social.oauth.return'),
+ path('oauth_login//', views.OAuthLoginView.as_view(), name='social.oauth.login'),
+ path('oauth_return/', views.OAuthReturnView.as_view(), name='social.oauth.return'),
path('control/global/social_auth/', views.SocialLoginView.as_view(), name='admin.global.social.auth.settings')
]
diff --git a/src/pretix/plugins/socialauth/views.py b/src/pretix/plugins/socialauth/views.py
index aa31053f7f..62bdddd258 100644
--- a/src/pretix/plugins/socialauth/views.py
+++ b/src/pretix/plugins/socialauth/views.py
@@ -1,14 +1,16 @@
import logging
from enum import StrEnum
-from urllib.parse import urlencode, urljoin, urlparse, urlunparse
+from urllib.parse import parse_qs, urlencode, urljoin, urlparse, urlunparse
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import SocialApp
from django.conf import settings
from django.contrib import messages
+from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
-from django.views.generic import TemplateView
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import TemplateView, View
from pydantic import ValidationError
from pretix.base.models import User
@@ -18,43 +20,99 @@
from pretix.helpers.urls import build_absolute_uri
from .schemas.login_providers import LoginProviders
+from .schemas.oauth2_params import OAuth2Params
logger = logging.getLogger(__name__)
adapter = get_adapter()
-def oauth_login(request, provider):
- gs = GlobalSettingsObject()
- client_id = gs.settings.get('login_providers', as_type=dict).get(provider, {}).get('client_id')
- provider = adapter.get_provider(request, provider, client_id=client_id)
-
- base_url = provider.get_login_url(request)
- query_params = {
- "next": build_absolute_uri("plugins:socialauth:social.oauth.return")
- }
- parsed_url = urlparse(base_url)
- updated_url = parsed_url._replace(query=urlencode(query_params))
- return redirect(urlunparse(updated_url))
+class OAuthLoginView(View):
+ def get(self, request: HttpRequest, provider: str) -> HttpResponse:
+ self.set_oauth2_params(request)
+ gs = GlobalSettingsObject()
+ client_id = (
+ gs.settings.get("login_providers", as_type=dict)
+ .get(provider, {})
+ .get("client_id")
+ )
+ provider_instance = adapter.get_provider(request, provider, client_id=client_id)
+
+ base_url = provider_instance.get_login_url(request)
+ query_params = {
+ "next": build_absolute_uri("plugins:socialauth:social.oauth.return")
+ }
+ parsed_url = urlparse(base_url)
+ updated_url = parsed_url._replace(query=urlencode(query_params))
+ return redirect(urlunparse(updated_url))
+
+ @staticmethod
+ def set_oauth2_params(request: HttpRequest) -> None:
+ """
+ Handle Login with SSO button from other components
+ This function will set 'oauth2_params' in session for oauth2_callback
+ """
+ next_url = request.GET.get("next", "")
+ if not next_url:
+ return
+
+ parsed = urlparse(next_url)
+
+ # Only allow relative URLs
+ if parsed.netloc or parsed.scheme:
+ return
+
+ params = parse_qs(parsed.query)
+ sanitized_params = {
+ k: v[0]
+ for k, v in params.items()
+ if k in OAuth2Params.model_fields.keys()
+ }
+
+ try:
+ oauth2_params = OAuth2Params.model_validate(sanitized_params)
+ request.session["oauth2_params"] = oauth2_params.model_dump()
+ except ValidationError as e:
+ logger.warning("Ignore invalid OAuth2 parameters: %s.", e)
+
+
+class OAuthReturnView(View):
+ def get(self, request: HttpRequest) -> HttpResponse:
+ try:
+ user = self.get_or_create_user(request)
+ response = process_login_and_set_cookie(request, user, False)
+ oauth2_params = request.session.pop("oauth2_params", {})
+ if oauth2_params:
+ try:
+ oauth2_params = OAuth2Params.model_validate(oauth2_params)
+ query_string = urlencode(oauth2_params.model_dump())
+ auth_url = reverse("control:oauth2_provider.authorize")
+ return redirect(f"{auth_url}?{query_string}")
+ except ValidationError as e:
+ logger.warning("Ignore invalid OAuth2 parameters: %s.", e)
+
+ return response
+ except AttributeError as e:
+ messages.error(
+ request, _("Error while authorizing: no email address available.")
+ )
+ logger.error("Error while authorizing: %s", e)
+ return redirect("control:auth.login")
-def oauth_return(request):
- try:
- user, _ = User.objects.get_or_create(
+ @staticmethod
+ def get_or_create_user(request: HttpRequest) -> User:
+ """
+ Get or create a user from social auth information.
+ """
+ return User.objects.get_or_create(
email=request.user.email,
defaults={
- 'locale': getattr(request, 'LANGUAGE_CODE', settings.LANGUAGE_CODE),
- 'timezone': getattr(request, 'timezone', settings.TIME_ZONE),
- 'auth_backend': 'native',
- 'password': '',
+ "locale": getattr(request, "LANGUAGE_CODE", settings.LANGUAGE_CODE),
+ "timezone": getattr(request, "timezone", settings.TIME_ZONE),
+ "auth_backend": "native",
+ "password": "",
},
- )
- return process_login_and_set_cookie(request, user, False)
- except AttributeError:
- messages.error(
- request, _('Error while authorizing: no email address available.')
- )
- logger.error('Error while authorizing: user has no email address.')
- return redirect('control:auth.login')
+ )[0]
class SocialLoginView(AdministratorPermissionRequiredMixin, TemplateView):