diff --git a/.env.example b/.env.example index 4199907..d4f6da8 100644 --- a/.env.example +++ b/.env.example @@ -31,3 +31,10 @@ ENABLE_SOFT_DELETE=false KEEP_DELETED_TRANSACTIONS_FOR=365 TASK_WORKERS=1 # This only work if you're using the single container option. Increase to have more open queues via procrastinate, you probably don't need to increase this. + +# OIDC Configuration. Uncomment the lines below if you want to add OIDC login to your instance +#OIDC_CLIENT_NAME="" +#OIDC_CLIENT_ID="" +#OIDC_CLIENT_SECRET="" +#OIDC_SERVER_URL="" +#OIDC_ALLOW_SIGNUP=true diff --git a/README.md b/README.md index f93ca56..7a7a42c 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,31 @@ To create the first user, open the container's console using Unraid's UI, by cli | ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. | | ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. | +## OIDC Configuration + +WYGIWYH supports login via OpenID Connect (OIDC) through `django-allauth`. This allows users to authenticate using an external OIDC provider. + +> [!NOTE] +> Currently only OpenID Connect is supported as a provider, open an issue if you need something else. + +To configure OIDC, you need to set the following environment variables: + +| Variable | Description | +|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `OIDC_CLIENT_NAME` | The name of the provider. will be displayed in the login page. Defaults to `OpenID Connect` | +| `OIDC_CLIENT_ID` | The Client ID provided by your OIDC provider. | +| `OIDC_CLIENT_SECRET` | The Client Secret provided by your OIDC provider. | +| `OIDC_SERVER_URL` | The base URL of your OIDC provider's discovery document or authorization server (e.g., `https://your-provider.com/auth/realms/your-realm`). `django-allauth` will use this to discover the necessary endpoints (authorization, token, userinfo, etc.). | +| `OIDC_ALLOW_SIGNUP` | Allow the automatic creation of inexistent accounts on a successfull authentication. Defaults to `true`. | + +**Callback URL (Redirect URI):** + +When configuring your OIDC provider, you will need to provide a callback URL (also known as a Redirect URI). For WYGIWYH, the default callback URL is: + +`https://your.wygiwyh.domain/daa/accounts/oidc//login/callback/` + +Replace `https://your.wygiwyh.domain` with the actual URL where your WYGIWYH instance is accessible. And `` with the slugfied value set in OIDC_CLIENT_NAME or the default `openid-connect` if you haven't set this variable. + # How it works Check out our [Wiki](https://github.com/eitchtee/WYGIWYH/wiki) for more information. diff --git a/app/WYGIWYH/settings.py b/app/WYGIWYH/settings.py index be6886b..79d3e89 100644 --- a/app/WYGIWYH/settings.py +++ b/app/WYGIWYH/settings.py @@ -14,6 +14,7 @@ import os import sys from pathlib import Path +from django.utils.text import slugify SITE_TITLE = "WYGIWYH" TITLE_SEPARATOR = "::" @@ -42,6 +43,7 @@ INSTALLED_APPS = [ "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", + "django.contrib.sites", "whitenoise.runserver_nostatic", "django.contrib.staticfiles", "webpack_boilerplate", @@ -61,7 +63,6 @@ INSTALLED_APPS = [ "apps.transactions.apps.TransactionsConfig", "apps.currencies.apps.CurrenciesConfig", "apps.accounts.apps.AccountsConfig", - "apps.common.apps.CommonConfig", "apps.net_worth.apps.NetWorthConfig", "apps.import_app.apps.ImportConfig", "apps.export_app.apps.ExportConfig", @@ -74,8 +75,15 @@ INSTALLED_APPS = [ "apps.calendar_view.apps.CalendarViewConfig", "apps.dca.apps.DcaConfig", "pwa", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.openid_connect", + "apps.common.apps.CommonConfig", ] +SITE_ID = 1 + MIDDLEWARE = [ "django_browser_reload.middleware.BrowserReloadMiddleware", "apps.common.middleware.thread_local.ThreadLocalMiddleware", @@ -91,6 +99,7 @@ MIDDLEWARE = [ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "hijack.middleware.HijackUserMiddleware", + "allauth.account.middleware.AccountMiddleware", ] ROOT_URLCONF = "WYGIWYH.urls" @@ -307,6 +316,42 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" LOGIN_REDIRECT_URL = "/" LOGIN_URL = "/login/" +LOGOUT_REDIRECT_URL = "/login/" + +# Allauth settings +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", # Keep default + "allauth.account.auth_backends.AuthenticationBackend", +] + +SOCIALACCOUNT_PROVIDERS = {"openid_connect": {"APPS": []}} + +if ( + os.getenv("OIDC_CLIENT_ID") + and os.getenv("OIDC_CLIENT_SECRET") + and os.getenv("OIDC_SERVER_URL") +): + SOCIALACCOUNT_PROVIDERS["openid_connect"]["APPS"].append( + { + "provider_id": slugify(os.getenv("OIDC_CLIENT_NAME", "OpenID Connect")), + "name": os.getenv("OIDC_CLIENT_NAME", "OpenID Connect"), + "client_id": os.getenv("OIDC_CLIENT_ID"), + "secret": os.getenv("OIDC_CLIENT_SECRET"), + "settings": { + "server_url": os.getenv("OIDC_SERVER_URL"), + }, + } + ) + +ACCOUNT_LOGIN_METHODS = {"email"} +ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"] +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +ACCOUNT_EMAIL_VERIFICATION = "none" +SOCIALACCOUNT_LOGIN_ON_GET = True +SOCIALACCOUNT_ONLY = True +SOCIALACCOUNT_AUTO_SIGNUP = os.getenv("OIDC_ALLOW_SIGNUP", "true").lower() == "true" +ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter" +SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter" # CRISPY FORMS CRISPY_ALLOWED_TEMPLATE_PACKS = ["bootstrap5", "crispy_forms/pure_text"] diff --git a/app/WYGIWYH/urls.py b/app/WYGIWYH/urls.py index a9612fb..f0294ee 100644 --- a/app/WYGIWYH/urls.py +++ b/app/WYGIWYH/urls.py @@ -21,6 +21,8 @@ from drf_spectacular.views import ( SpectacularAPIView, SpectacularSwaggerView, ) +from allauth.socialaccount.providers.openid_connect.views import login, callback + urlpatterns = [ path("admin/", admin.site.urls), @@ -36,6 +38,13 @@ urlpatterns = [ SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui", ), + path("auth/", include("allauth.urls")), # allauth urls + # path("auth/oidc//login/", login, name="openid_connect_login"), + # path( + # "auth/oidc//login/callback/", + # callback, + # name="openid_connect_callback", + # ), path("", include("apps.transactions.urls")), path("", include("apps.common.urls")), path("", include("apps.users.urls")), diff --git a/app/apps/common/apps.py b/app/apps/common/apps.py index df04969..76c08b2 100644 --- a/app/apps/common/apps.py +++ b/app/apps/common/apps.py @@ -4,3 +4,17 @@ from django.apps import AppConfig class CommonConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.common" + + def ready(self): + from django.contrib import admin + from django.contrib.sites.models import Site + from allauth.socialaccount.models import ( + SocialAccount, + SocialApp, + SocialToken, + ) + + admin.site.unregister(Site) + admin.site.unregister(SocialAccount) + admin.site.unregister(SocialApp) + admin.site.unregister(SocialToken) diff --git a/app/apps/users/migrations/0021_alter_usersettings_timezone.py b/app/apps/users/migrations/0021_alter_usersettings_timezone.py index 1eba65e..d8c1dc2 100644 --- a/app/apps/users/migrations/0021_alter_usersettings_timezone.py +++ b/app/apps/users/migrations/0021_alter_usersettings_timezone.py @@ -1,5 +1,6 @@ # Generated by Django 5.1.11 on 2025-06-20 03:57 + from django.db import migrations, models diff --git a/app/templates/users/login.html b/app/templates/users/login.html index 341884c..8c74dc0 100644 --- a/app/templates/users/login.html +++ b/app/templates/users/login.html @@ -2,6 +2,7 @@ {% load i18n %} {% load settings %} {% load crispy_forms_tags %} +{% load socialaccount %} {% block title %}Login{% endblock %} @@ -25,6 +26,24 @@

Login

{% crispy form %} + + {% get_providers as socialaccount_providers %} + {% if socialaccount_providers %} +
+
+ +
+ {% endif %}
diff --git a/requirements.txt b/requirements.txt index 67eded0..06a7a5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ watchfiles==0.24.0 # https://github.com/samuelcolvin/watchfiles procrastinate[django]~=2.15.1 requests~=2.32.3 +django-allauth[socialaccount]~=65.9.0 pytz python-dateutil~=2.9.0.post0