Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create speaker account with SSO as part of the answer to Call for Proposals #508

Closed
wants to merge 9 commits into from
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 = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this step if you have Pydantic model. You can tell Pydantic to automatically strip whitespace.

from typing import Annotated

from pydantic import BaseModel, StringConstraints


class OAuth2Params(BaseModel):
     response_type: Annotated[str, StringConstraints(strip_whitespace=True)] = 'code'

Learn more here.

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
Loading