From 997fd760d2d2b7b2348d84e31366a15509a9b368 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 17 May 2026 06:36:48 +0200 Subject: [PATCH] fix oauth --- .env.example | 12 +++++-- allauth_oauth/provider.py | 7 ++-- allauth_oauth/signals.py | 24 ++++++++++++- allauth_oauth/views.py | 16 ++++++--- docker-compose.yml | 17 +++------ photo21/settings.py | 35 ++++++++++++++----- photo21/templates/account/login.html | 6 +++- photo21/templates/base.html | 7 ++-- .../socialaccount/snippets/provider_list.html | 2 +- photo21/urls.py | 2 ++ photo21/views.py | 8 +++++ 11 files changed, 99 insertions(+), 37 deletions(-) diff --git a/.env.example b/.env.example index 930af5b..6b2a2a4 100644 --- a/.env.example +++ b/.env.example @@ -39,7 +39,15 @@ OAUTH_ONLY=False #OAUTH_BUTTON_TEXT=Login with OAuth #OAUTH_BUTTON_IMAGE= # Space-separated OAuth2 scopes -#OAUTH_SCOPE=openid profile email +#OAUTH_SCOPE=openid profile email groups +# Comma-separated OAuth groups that grant Django staff (admin) access +#OAUTH_STAFF_GROUPS=admins moderators +# Map OAuth groups to Django groups: space-separated oauth_group:django_group pairs +#OAUTH_GROUP_MAP=editors:Editors photographers:Photographers +# Override OAuth2 endpoint URLs (default: authentik-style /application/o/...) +#OAUTH_AUTHORIZE_URL=https://auth.example.com/application/o/authorize/ +#OAUTH_TOKEN_URL=https://auth.example.com/application/o/token/ +#OAUTH_PROFILE_URL=https://auth.example.com/application/o/userinfo/ # Database engine: 'sqlite' or 'postgres' DB_ENGINE=sqlite @@ -48,7 +56,7 @@ DB_ENGINE=sqlite #DB_NAME=photo21 #DB_USER=photo21 #DB_PASSWORD= -#DB_HOST=localhost +#DB_HOST=db #DB_PORT=5432 # SQLite settings (only used when DB_ENGINE=sqlite) diff --git a/allauth_oauth/provider.py b/allauth_oauth/provider.py index 6a6430c..b355f41 100644 --- a/allauth_oauth/provider.py +++ b/allauth_oauth/provider.py @@ -6,7 +6,6 @@ from allauth.account.models import EmailAddress from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider - class OAuthAccount(ProviderAccount): def to_str(self): return self.account.extra_data.get("username") @@ -18,14 +17,12 @@ class OAuthProvider(OAuth2Provider): account_class = OAuthAccount def extract_uid(self, data): - return str(data["username"]) + return str(data["preferred_username"]) def extract_common_fields(self, data): return dict( email=data.get("email"), - username=data.get("username"), - last_name=data.get("last_name"), - first_name=data.get("first_name"), + username=data.get("preferred_username"), ) def get_default_scope(self): diff --git a/allauth_oauth/signals.py b/allauth_oauth/signals.py index d7f1cf6..30ebf4e 100644 --- a/allauth_oauth/signals.py +++ b/allauth_oauth/signals.py @@ -2,7 +2,10 @@ # Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from allauth.account.models import EmailAddress from allauth.socialaccount.signals import pre_social_login +from django.conf import settings +from django.contrib.auth.models import Group from django.dispatch import receiver @@ -19,11 +22,30 @@ def sync_user_fields(sender, request, sociallogin, **kwargs): if email and user.email != email: user.email = email changed = True + EmailAddress.objects.filter(user=user).update(email=email) - username = data.get("username") + username = data.get("preferred_username") if username and user.username != username: user.username = username changed = True + staff_groups = settings.OAUTH_STAFF_GROUPS + if staff_groups: + oauth_groups = set(data.get("groups", [])) + is_staff = bool(oauth_groups & set(staff_groups)) + if user.is_staff != is_staff: + user.is_staff = is_staff + changed = True + if changed: user.save() + + group_map = settings.OAUTH_GROUP_MAP + if group_map: + oauth_groups = set(data.get("groups", [])) + for oauth_group, django_group_name in group_map.items(): + django_group, _ = Group.objects.get_or_create(name=django_group_name) + if oauth_group in oauth_groups: + user.groups.add(django_group) + else: + user.groups.remove(django_group) diff --git a/allauth_oauth/views.py b/allauth_oauth/views.py index 7a568a5..20efbfb 100644 --- a/allauth_oauth/views.py +++ b/allauth_oauth/views.py @@ -4,6 +4,7 @@ import requests from allauth.socialaccount import app_settings +from django.core.exceptions import ImproperlyConfigured from allauth.socialaccount.providers.oauth2.views import ( OAuth2Adapter, OAuth2CallbackView, @@ -31,20 +32,27 @@ class OAuthAdapter(OAuth2Adapter): @property def domain(self): - return self.settings.get("DOMAIN", "") + domain = self.settings.get("DOMAIN", "") + if not domain: + raise ImproperlyConfigured( + "OAUTH_SERVER_URL is not configured. Set it in your .env file." + ) + return domain @property def access_token_url(self): - return f"https://{self.domain}/o/token/" + return self.settings.get("TOKEN_URL", f"https://{self.domain}/application/o/token/") @property def authorize_url(self): - return f"https://{self.domain}/o/authorize/" + return self.settings.get("AUTHORIZE_URL", f"https://{self.domain}/application/o/authorize/") @property def profile_url(self): - return f"https://{self.domain}/api/me/" + return self.settings.get("PROFILE_URL", f"https://{self.domain}/application/o/userinfo/") +OAuthProvider.oauth2_adapter_class = OAuthAdapter + oauth2_login = OAuth2LoginView.adapter_view(OAuthAdapter) oauth2_callback = OAuth2CallbackView.adapter_view(OAuthAdapter) diff --git a/docker-compose.yml b/docker-compose.yml index 001fed7..36d26c4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,10 +8,11 @@ services: image: postgres:16 container_name: photo26-db restart: unless-stopped + env_file: .env environment: - POSTGRES_DB: photo26 - POSTGRES_USER: photo26 - POSTGRES_PASSWORD: change-me + POSTGRES_DB: ${DB_NAME:-photo26} + POSTGRES_USER: ${DB_USER:-photo26} + POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - ./postgres_data:/var/lib/postgresql/data networks: @@ -23,15 +24,7 @@ services: restart: unless-stopped depends_on: - db - environment: - DB_ENGINE: postgres - DB_NAME: photo26 - DB_USER: photo26 - DB_PASSWORD: change-me - DB_HOST: db - DB_PORT: 5432 - SECRET_KEY: change-me - EXTRA_HOSTS: photos.example.org + env_file: .env volumes: - ./media:/app/media ports: diff --git a/photo21/settings.py b/photo21/settings.py index 422f2c7..4c1cae6 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -12,6 +12,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.2/ref/settings/ """ +import json import os from decouple import Csv, config @@ -64,6 +65,15 @@ OAUTH_SERVER_URL = config("OAUTH_SERVER_URL", default="") OAUTH_BUTTON_TEXT = config("OAUTH_BUTTON_TEXT", default="Login with OAuth") OAUTH_BUTTON_IMAGE = config("OAUTH_BUTTON_IMAGE", default="") OAUTH_SCOPE = config("OAUTH_SCOPE", default="openid profile email", cast=Csv(delimiter=" ")) +OAUTH_STAFF_GROUPS = config("OAUTH_STAFF_GROUPS", default="", cast=Csv(delimiter=" ")) +OAUTH_GROUP_MAP = dict( + pair.split(":", 1) + for pair in config("OAUTH_GROUP_MAP", default="", cast=Csv(delimiter=" ")) + if ":" in pair +) +OAUTH_AUTHORIZE_URL = config("OAUTH_AUTHORIZE_URL", default="") +OAUTH_TOKEN_URL = config("OAUTH_TOKEN_URL", default="") +OAUTH_PROFILE_URL = config("OAUTH_PROFILE_URL", default="") INSTALLED_APPS = [ "django.contrib.admin", @@ -118,6 +128,7 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "photo21.views.oauth_context", ], }, }, @@ -282,16 +293,24 @@ ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"} if OAUTH_ENABLED: SOCIALACCOUNT_ONLY = OAUTH_ONLY - SOCIALACCOUNT_PROVIDERS = { - "oauth": { - "SCOPE": OAUTH_SCOPE, - "DOMAIN": OAUTH_SERVER_URL, - "APP": { - "client_id": OAUTH_CLIENT_ID, - "secret": OAUTH_CLIENT_SECRET, - }, + if OAUTH_ONLY: + ACCOUNT_EMAIL_VERIFICATION = 'none' + SOCIALACCOUNT_LOGIN_ON_GET = True + _oauth_provider = { + "SCOPE": OAUTH_SCOPE, + "DOMAIN": OAUTH_SERVER_URL, + "APP": { + "client_id": OAUTH_CLIENT_ID, + "secret": OAUTH_CLIENT_SECRET, }, } + if OAUTH_AUTHORIZE_URL: + _oauth_provider["AUTHORIZE_URL"] = OAUTH_AUTHORIZE_URL + if OAUTH_TOKEN_URL: + _oauth_provider["TOKEN_URL"] = OAUTH_TOKEN_URL + if OAUTH_PROFILE_URL: + _oauth_provider["PROFILE_URL"] = OAUTH_PROFILE_URL + SOCIALACCOUNT_PROVIDERS = {"oauth": _oauth_provider} # Use Bootstrap forms CRISPY_TEMPLATE_PACK = "bootstrap4" diff --git a/photo21/templates/account/login.html b/photo21/templates/account/login.html index a2bc666..b1c48b3 100644 --- a/photo21/templates/account/login.html +++ b/photo21/templates/account/login.html @@ -15,10 +15,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
+ {% if not SOCIALACCOUNT_ONLY %}

{% blocktrans trimmed with site.name as site_name %}Please sign in with one of your existing third party accounts. Or, sign up for a {{ site_name }} account and sign in below:{% endblocktrans %}

+ {% endif %} {% include "socialaccount/snippets/login_extra.html" %} @@ -27,6 +29,7 @@ SPDX-License-Identifier: GPL-3.0-or-later sign up first.{% endblocktrans %}

{% endif %} + {% if not SOCIALACCOUNT_ONLY %}
{% csrf_token %} {{ form|crispy }} {% if redirect_field_value %} @@ -36,7 +39,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
- {% trans "Forgot Password?" %} + {% url 'account_reset_password' as reset_url %}{% if reset_url %}{% trans "Forgot Password?" %}{% endif %} + {% endif %} diff --git a/photo21/templates/base.html b/photo21/templates/base.html index ccd17c2..ce771a1 100644 --- a/photo21/templates/base.html +++ b/photo21/templates/base.html @@ -100,15 +100,16 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "Log in" %} - + {% endif %} {% endif %} diff --git a/photo21/templates/socialaccount/snippets/provider_list.html b/photo21/templates/socialaccount/snippets/provider_list.html index 71a8ec0..dc6aad7 100644 --- a/photo21/templates/socialaccount/snippets/provider_list.html +++ b/photo21/templates/socialaccount/snippets/provider_list.html @@ -17,5 +17,5 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endfor %} {% endif %} {% trans "Sign in with" %} {{provider.name}} + href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">{{ OAUTH_BUTTON_TEXT|default:provider.name }} {% endfor %} diff --git a/photo21/urls.py b/photo21/urls.py index 2884442..5b4df26 100644 --- a/photo21/urls.py +++ b/photo21/urls.py @@ -24,6 +24,8 @@ urlpatterns = [ path("", IndexView.as_view(), name="index"), path("", include("photologue.urls", namespace="photologue")), path("accounts/", include("allauth.urls")), + path("accounts/", include("allauth_oauth.urls")), + path("i18n/", include("django.conf.urls.i18n")), path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), path("admin/doc/", include("django.contrib.admindocs.urls")), diff --git a/photo21/views.py b/photo21/views.py index 65f0b11..9f43f67 100644 --- a/photo21/views.py +++ b/photo21/views.py @@ -69,3 +69,11 @@ class IndexView(LoginRequiredMixin, ListView): context["superusers"] = superusers return context + + +def oauth_context(request): + return { + "OAUTH_BUTTON_TEXT": settings.OAUTH_BUTTON_TEXT, + "OAUTH_BUTTON_IMAGE": settings.OAUTH_BUTTON_IMAGE, + "SOCIALACCOUNT_ONLY": settings.OAUTH_ONLY, + }