feat(auth): trust OIDC connections and connect them with local accounts

This commit is contained in:
Herculino Trotta
2026-02-15 14:41:45 -03:00
parent 27e85c4776
commit ea097ab6f0
3 changed files with 85 additions and 1 deletions

View File

@@ -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 = [

View File

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