1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
Backported and adapted patch for py-social-auth-core 5.4.3 to fix
CVE-2025-61783.
Obtained from:
From 10c80e2ebabeccd4e9c84ad0e16e1db74148ed4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= <michal@cihar.com>
Date: Tue, 30 Sep 2025 13:38:21 +0200
Subject: [PATCH] fix: avoid associating with existing user when creating fails
This behavior was introduced in 9f86059e9d8070bc5ecd7ba069fadab1c9bf502a
to workaround concurrency issues, but the only safe way to deal with
this is to restart the pipeline to make sure that all possible policies
apply. This is currently not possible, so let's fail with
AuthAlreadyAssociated and let user restart the authentication pipeline
manually.
--- social_django/storage.py.orig 2025-02-13 13:06:56 UTC
+++ social_django/storage.py
@@ -5,6 +5,7 @@ from django.db.utils import IntegrityError
from django.core.exceptions import FieldDoesNotExist
from django.db import router, transaction
from django.db.utils import IntegrityError
+from social_core.exceptions import AuthAlreadyAssociated
from social_core.storage import (
AssociationMixin,
BaseStorage,
@@ -75,26 +76,24 @@ class DjangoUserMixin(UserMixin):
cls.user_model()._meta.get_field("username")
except FieldDoesNotExist:
kwargs.pop("username")
+
+ if hasattr(transaction, "atomic"):
+ # In Django versions that have an "atomic" transaction decorator / context
+ # manager, there's a transaction wrapped around this call.
+ # If the create fails below due to an IntegrityError, ensure that the transaction
+ # stays undamaged by wrapping the create in an atomic.
+ using = router.db_for_write(cls.user_model())
+
try:
if hasattr(transaction, "atomic"):
- # In Django versions that have an "atomic" transaction decorator / context
- # manager, there's a transaction wrapped around this call.
- # If the create fails below due to an IntegrityError, ensure that the transaction
- # stays undamaged by wrapping the create in an atomic.
- using = router.db_for_write(cls.user_model())
with transaction.atomic(using=using):
user = cls.user_model()._default_manager.create_user(*args, **kwargs)
else:
user = cls.user_model()._default_manager.create_user(*args, **kwargs)
+
+ return user
except IntegrityError as exc:
- # If email comes in as None it won't get found in the get
- if kwargs.get("email", True) is None:
- kwargs["email"] = ""
- try:
- user = cls.user_model()._default_manager.get(*args, **kwargs)
- except cls.user_model().DoesNotExist:
- raise exc
- return user
+ raise AuthAlreadyAssociated(None) from exc
@classmethod
def get_user(cls, pk=None, **kwargs):
--- tests/test_models.py.orig 2025-02-13 13:06:56 UTC
+++ tests/test_models.py
@@ -5,6 +5,7 @@ from django.test import TestCase
from django.core.management import call_command
from django.db import IntegrityError
from django.test import TestCase
+from social_core.exceptions import AuthAlreadyAssociated
from social_django.models import (
AbstractUserSocialAuth,
@@ -101,17 +102,21 @@ class TestUserSocialAuth(TestCase):
self.assertEqual(UserSocialAuth.get_username(self.user), self.user.username)
def test_create_user(self):
- # Catch integrity error and find existing user
- UserSocialAuth.create_user(username=self.user.username)
+ UserSocialAuth.create_user(username="testuser")
def test_create_user_reraise(self):
- with self.assertRaises(IntegrityError):
+ with self.assertRaises(AuthAlreadyAssociated):
UserSocialAuth.create_user(username=self.user.username, email=None)
@mock.patch("social_django.models.UserSocialAuth.username_field", return_value="email")
- @mock.patch("django.contrib.auth.models.UserManager.create_user", side_effect=IntegrityError)
+ @mock.patch("django.contrib.auth.models.UserManager.create_user", return_value="<User>")
def test_create_user_custom_username(self, *args):
UserSocialAuth.create_user(username=self.user.email)
+
+ @mock.patch("django.contrib.auth.models.UserManager.create_user", side_effect=IntegrityError)
+ def test_create_user_existing(self, *args):
+ with self.assertRaises(AuthAlreadyAssociated):
+ UserSocialAuth.create_user(username=self.user.email)
@mock.patch("social_django.storage.transaction", spec=[])
def test_create_user_without_transaction_atomic(self, *args):
|