Skip to content

Commit

Permalink
Create speaker account with SSO as part of the answer to Call for Pro…
Browse files Browse the repository at this point in the history
…posals (#520)

* Make social login redirect to previous page

* Add url validate

* Add scheme check for url validation

* Resolve conversations

* Resolve conversations

* Resolve conversations
  • Loading branch information
HungNgien authored Jan 25, 2025
1 parent d821e0a commit de08ff0
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 32 deletions.
3 changes: 2 additions & 1 deletion src/pretix/control/templates/pretixcontrol/auth/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
{% if login_providers %}
{% for provider, settings in login_providers.items %}
{% if settings.state %}
<a href='{% url "plugins:socialauth:social.oauth.login" provider %}' data-method="post" class="btn btn-primary btn-block">
<a href='{% url "plugins:socialauth:social.oauth.login" provider %}{% append_next request.GET.next %}'
data-method="post" class="btn btn-primary btn-block">
{% with provider|capfirst as provider_capitalized %}
{% blocktrans %}Login with {{ provider_capitalized }}{% endblocktrans %}
{% endwith %}
Expand Down
11 changes: 11 additions & 0 deletions src/pretix/plugins/socialauth/schemas/oauth2_params.py
Original file line number Diff line number Diff line change
@@ -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)]
4 changes: 2 additions & 2 deletions src/pretix/plugins/socialauth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from . import views

urlpatterns = [
path('oauth_login/<str:provider>/', views.oauth_login, name='social.oauth.login'),
path('oauth_return/', views.oauth_return, name='social.oauth.return'),
path('oauth_login/<str:provider>/', 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')
]
116 changes: 87 additions & 29 deletions src/pretix/plugins/socialauth/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down

0 comments on commit de08ff0

Please sign in to comment.