From ea097ab6f0f56332dac2dfdd2caa2782d42a9911 Mon Sep 17 00:00:00 2001 From: Herculino Trotta Date: Sun, 15 Feb 2026 14:41:45 -0300 Subject: [PATCH] feat(auth): trust OIDC connections and connect them with local accounts --- README.md | 7 ++++ app/WYGIWYH/settings.py | 4 +- app/apps/users/adapters.py | 75 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 app/apps/users/adapters.py diff --git a/README.md b/README.md index 9835c2b..49123e8 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,13 @@ WYGIWYH supports login via OpenID Connect (OIDC) through `django-allauth`. This > [!NOTE] > Currently only OpenID Connect is supported as a provider, open an issue if you need something else. +> [!Caution] +> WYGIWYH automatically connects OIDC accounts to existing local accounts with matching email addresses. +> This means if a user already exists with email `user@example.com` and someone logs in via OIDC with the same email, the OIDC account will be automatically linked to the existing account without requiring user confirmation. +> This is only recommended for trusted OIDC providers that verify email addresses and where you control who can create accounts. + +### Configuration + To configure OIDC, you need to set the following environment variables: | Variable | Description | diff --git a/app/WYGIWYH/settings.py b/app/WYGIWYH/settings.py index eead651..0cf8373 100644 --- a/app/WYGIWYH/settings.py +++ b/app/WYGIWYH/settings.py @@ -375,8 +375,10 @@ ACCOUNT_EMAIL_VERIFICATION = "none" SOCIALACCOUNT_LOGIN_ON_GET = True SOCIALACCOUNT_ONLY = True SOCIALACCOUNT_AUTO_SIGNUP = os.getenv("OIDC_ALLOW_SIGNUP", "true").lower() == "true" +SOCIALACCOUNT_EMAIL_AUTHENTICATION = True +SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter" -SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter" +SOCIALACCOUNT_ADAPTER = "apps.users.adapters.AutoConnectSocialAccountAdapter" # CRISPY FORMS CRISPY_ALLOWED_TEMPLATE_PACKS = [ diff --git a/app/apps/users/adapters.py b/app/apps/users/adapters.py new file mode 100644 index 0000000..41b02c1 --- /dev/null +++ b/app/apps/users/adapters.py @@ -0,0 +1,75 @@ +import logging +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.contrib.auth import get_user_model + +User = get_user_model() +logger = logging.getLogger(__name__) + + +class AutoConnectSocialAccountAdapter(DefaultSocialAccountAdapter): + """ + Custom adapter to automatically connect social accounts to existing users + with the same email address. + + SECURITY WARNING: + This adapter automatically connects OIDC accounts to existing local accounts + based on email matching. + + If your OIDC provider allows unverified emails, this could lead to + ACCOUNT TAKEOVER attacks where an attacker creates an OIDC account + with someone else's email and gains access to their account. + """ + + def pre_social_login(self, request, sociallogin): + """ + Invoked just after a user successfully authenticates via a + social provider, but before the login is actually processed. + + If a user with the same email already exists, connect the social + account to that existing user instead of creating a new account. + """ + # If the social account is already connected to a user, do nothing + if sociallogin.is_existing: + return + + # Check if we have an email from the social provider + if not sociallogin.email_addresses: + logger.warning( + "OIDC login attempted without email address. " + f"Provider: {sociallogin.account.provider}" + ) + return + + # Get the email from the social login + email = sociallogin.email_addresses[0].email.lower() + + # Try to find an existing user with this email + try: + user = User.objects.get(email__iexact=email) + + # Log this connection for security audit trail + logger.info( + f"Auto-connecting OIDC account to existing user. " + f"Email: {email}, Provider: {sociallogin.account.provider}, " + f"User ID: {user.id}" + ) + + # Connect the social account to the existing user + sociallogin.connect(request, user) + + except User.DoesNotExist: + # No user with this email exists, proceed with normal signup flow + logger.debug( + f"No existing user found for email {email}. " + "Proceeding with new account creation." + ) + pass + except User.MultipleObjectsReturned: + # Multiple users with the same email (shouldn't happen with unique constraint) + logger.error( + f"Multiple users found with email {email}. " + "This should not happen with unique constraint. " + "Blocking auto-connect." + ) + # Let the default behavior handle this + pass