fix oauth
All checks were successful
Docker / build (release) Successful in 9s

This commit is contained in:
krek0 2026-05-17 06:36:48 +02:00
parent 1de1cb4086
commit 997fd760d2
11 changed files with 99 additions and 37 deletions

View file

@ -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)

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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"

View file

@ -15,10 +15,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="d-grid col-6 mx-auto">
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
</div>
{% if not SOCIALACCOUNT_ONLY %}
<hr/>
<p>{% blocktrans trimmed with site.name as site_name %}Please sign in with one
of your existing third party accounts. Or, <a href="{{ signup_url }}">sign up</a>
for a {{ site_name }} account and sign in below:{% endblocktrans %}</p>
{% endif %}
{% include "socialaccount/snippets/login_extra.html" %}
@ -27,6 +29,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a href="{{ signup_url }}">sign up</a> first.{% endblocktrans %}</p>
{% endif %}
{% if not SOCIALACCOUNT_ONLY %}
<form method="post" action="{% url 'account_login' %}">{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
@ -36,7 +39,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<button type="submit" class="btn btn-primary btn-lg my-2">{% trans "Sign In" %}</button>
</div>
</form>
<a class="link-secondary" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
{% url 'account_reset_password' as reset_url %}{% if reset_url %}<a class="link-secondary" href="{{ reset_url }}">{% trans "Forgot Password?" %}</a>{% endif %}
{% endif %}
</div>
</div>
<!-- <p class="small text-center mt-1">{% trans "If any problem, please contact the server owners at" %} <code>photos[at]crans.org</code>.</p> -->

View file

@ -100,15 +100,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans "Log in" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'account_signup' %}">
{% url 'account_signup' as signup_url %}
{% if signup_url %}<li class="nav-item">
<a class="nav-link" href="{{ signup_url }}">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-plus" viewBox="0 0 16 16">
<path d="M6 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H1s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C9.516 10.68 8.289 10 6 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
</svg>
{% trans "Sign up" %}
</a>
</li>
</li>{% endif %}
{% endif %}
</ul>
</div>

View file

@ -17,5 +17,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endfor %}
{% endif %}
<a title="{{provider.name}}" class="btn btn-success"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">{% trans "Sign in with" %} {{provider.name}}</a>
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">{{ OAUTH_BUTTON_TEXT|default:provider.name }}</a>
{% endfor %}

View file

@ -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")),

View file

@ -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,
}