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 %}
- {% 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" %}
-
-
+ {% url 'account_signup' as signup_url %}
+ {% if signup_url %}
+
{% trans "Sign up" %}
-
+ {% 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,
+ }