Skip to content

Commit

Permalink
fix(socialaccount): MultipleObjectsReturned
Browse files Browse the repository at this point in the history
  • Loading branch information
pennersr committed Feb 14, 2025
1 parent ae7ed78 commit cbd539c
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 28 deletions.
6 changes: 5 additions & 1 deletion ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
65.4.2 (unreleased)
*******************

- ...
Fixes
-----

- Headless: In case you had multiple apps of the same provider configured,
you could run into a ``MultipleObjectsReturned``. Fixed.


65.4.1 (2025-02-07)
Expand Down
7 changes: 7 additions & 0 deletions allauth/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,13 @@ def google_provider_settings(settings):
return gsettings


@pytest.fixture
def twitter_provider_settings(settings):
tsettings = {"APPS": [{"client_id": "client_id", "secret": "secret"}]}
settings.SOCIALACCOUNT_PROVIDERS = {"twitter": tsettings}
return tsettings


@pytest.fixture
def user_with_totp(user):
from allauth.mfa.totp.internal import auth
Expand Down
2 changes: 1 addition & 1 deletion allauth/headless/socialaccount/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def provider_flows(request):


def _signup_flow(request, sociallogin):
provider = sociallogin.account.get_provider()
provider = sociallogin.provider
flow = {
"id": Flow.PROVIDER_SIGNUP,
"provider": _provider_data(request, provider),
Expand Down
27 changes: 27 additions & 0 deletions allauth/headless/socialaccount/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,33 @@ def test_invalid_token(client, headless_reverse, db, google_provider_settings):
}


def test_valid_token_multiple_apps(
client, headless_reverse, db, google_provider_settings, settings, user_factory
):
settings.ACCOUNT_EMAIL_VERIFICATION = "mandatory"
google_provider_settings["APPS"].append(
{"client_id": "client_id2", "secret": "secret2"}
)
id_token = {"sub": "uid-from-id-token", "email": "a@b.com", "email_verified": True}
with patch(
"allauth.socialaccount.providers.google.views._verify_and_decode",
return_value=id_token,
):
resp = client.post(
headless_reverse("headless:socialaccount:provider_token"),
data={
"provider": "google",
"token": {
"id_token": "dummy",
"client_id": google_provider_settings["APPS"][0]["client_id"],
},
"process": AuthProcess.LOGIN,
},
content_type="application/json",
)
assert resp.status_code == 200


def test_auth_error_no_headless_request(client, db, google_provider_settings, settings):
"""Authentication errors use the regular "Third-Party Login Failure"
template if headless is not used.
Expand Down
2 changes: 1 addition & 1 deletion allauth/socialaccount/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ def can_authenticate_by_email(self, login, email):
(``SOCIALACCOUNT_PROVIDERS``).
"""
ret = None
provider = login.account.get_provider()
provider = login.provider
if provider.app:
ret = provider.app.settings.get("email_authentication")
if ret is None:
Expand Down
6 changes: 5 additions & 1 deletion allauth/socialaccount/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import (
SocialAccount,
SocialLogin,
Expand All @@ -25,8 +26,11 @@ def factory(
user = user_factory(
username=username, email=email, commit=False, with_email=with_email
)
provider_instance = get_adapter().get_provider(request=None, provider=provider)
account = SocialAccount(provider=provider, uid=uid)
sociallogin = SocialLogin(user=user, account=account)
sociallogin = SocialLogin(
provider=provider_instance, user=user, account=account
)
if with_email:
sociallogin.email_addresses = [
EmailAddress(email=user.email, verified=email_verified, primary=True)
Expand Down
2 changes: 1 addition & 1 deletion allauth/socialaccount/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def validate_unique_email(self, value):
return super(SignupForm, self).validate_unique_email(value)
except forms.ValidationError:
raise get_adapter().validation_error(
"email_taken", self.sociallogin.account.get_provider().name
"email_taken", self.sociallogin.provider.name
)


Expand Down
7 changes: 7 additions & 0 deletions allauth/socialaccount/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ def __init__(
account: Optional[SocialAccount] = None,
token: Optional[SocialToken] = None,
email_addresses: Optional[List[EmailAddress]] = None,
provider=None,
):
self.provider = provider
if token:
assert token.account is None or token.account == account # nosec
self.token = token
Expand Down Expand Up @@ -249,6 +251,7 @@ def is_headless(self) -> bool:
def serialize(self) -> Dict[str, Any]:
serialize_instance = get_adapter().serialize_instance
ret = dict(
provider=self.provider.serialize(),
account=serialize_instance(self.account),
user=serialize_instance(self.user),
state=self.state,
Expand All @@ -260,7 +263,10 @@ def serialize(self) -> Dict[str, Any]:

@classmethod
def deserialize(cls, data: Dict[str, Any]) -> "SocialLogin":
from allauth.socialaccount.providers.base.provider import Provider

deserialize_instance = get_adapter().deserialize_instance
provider = Provider.deserialize(data["provider"])
account = deserialize_instance(SocialAccount, data["account"])
user = deserialize_instance(get_user_model(), data["user"])
if "token" in data:
Expand All @@ -272,6 +278,7 @@ def deserialize(cls, data: Dict[str, Any]) -> "SocialLogin":
email_address = deserialize_instance(EmailAddress, ea)
email_addresses.append(email_address)
ret = cls()
ret.provider = provider
ret.token = token
ret.account = account
ret.user = user
Expand Down
21 changes: 19 additions & 2 deletions allauth/socialaccount/providers/base/provider.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Dict, Optional
from typing import Any, Dict, Optional

from django.core.exceptions import ImproperlyConfigured, PermissionDenied

from allauth.account.utils import get_next_redirect_url, get_request_param
from allauth.core import context
from allauth.socialaccount import app_settings
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.internal import statekit
Expand Down Expand Up @@ -128,7 +129,9 @@ def sociallogin_from_response(self, request, response):
if email:
common_fields["email"] = email
sociallogin = SocialLogin(
account=socialaccount, email_addresses=email_addresses
provider=self,
account=socialaccount,
email_addresses=email_addresses,
)
user = sociallogin.user = adapter.new_user(request, sociallogin)
user.set_unusable_password()
Expand Down Expand Up @@ -233,6 +236,20 @@ def sub_id(self) -> str:
(self.app.provider_id or self.app.provider) if self.uses_apps else self.id
)

def serialize(self) -> Dict[str, Any]:
ret = {"id": self.id}
if self.uses_apps:
ret["app.client_id"] = self.app.client_id
return ret

@classmethod
def deserialize(cls, data: Dict[str, Any]) -> "Provider":
return get_adapter().get_provider(
context.request,
provider=data["id"],
client_id=data.get("app.client_id"),
)


class ProviderAccount:
def __init__(self, social_account):
Expand Down
2 changes: 1 addition & 1 deletion allauth/socialaccount/tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def test_record_authentication(
sociallogin = sociallogin_factory(provider="unittest-server", uid="123")
sociallogin.state["process"] = process
sociallogin.token = SocialToken(
app=sociallogin.account.get_provider().app, token="123", token_secret="456"
app=sociallogin.provider.app, token="123", token_secret="456"
)
SocialAccount.objects.create(user=user, uid="123", provider="unittest-server")
request = request_factory.get("/")
Expand Down
31 changes: 11 additions & 20 deletions allauth/socialaccount/tests/test_signup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from allauth.account.utils import user_email, user_username
from allauth.core import context
from allauth.socialaccount.helpers import complete_social_login
from allauth.socialaccount.models import SocialAccount, SocialLogin
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.views import signup


Expand All @@ -34,7 +34,9 @@ def f(client, sociallogin):


@pytest.fixture
def email_address_clash(request_factory):
def email_address_clash(
request_factory, sociallogin_factory, twitter_provider_settings
):
def _email_address_clash(username, email):
User = get_user_model()
# Some existig user
Expand All @@ -48,12 +50,8 @@ def _email_address_clash(username, email):
)

# A social user being signed up...
account = SocialAccount(provider="twitter", uid="123")
user = User()
user_username(user, username)
user_email(user, email)
sociallogin = SocialLogin(
user=user, account=account, email_addresses=[EmailAddress(email=email)]
sociallogin = sociallogin_factory(
provider="twitter", username=username, email=email
)

# Signing up, should pop up the social signup form
Expand Down Expand Up @@ -146,7 +144,9 @@ def test_email_address_clash_username_auto_signup(db, settings, email_address_cl
assert user_username(user) != "test"


def test_populate_username_in_blacklist(db, settings, request_factory):
def test_populate_username_in_blacklist(
db, settings, request_factory, sociallogin_factory, twitter_provider_settings
):
settings.ACCOUNT_EMAIL_REQUIRED = True
settings.ACCOUNT_USERNAME_BLACKLIST = ["username", "username1", "username2"]
settings.ACCOUNT_UNIQUE_EMAIL = True
Expand All @@ -155,18 +155,9 @@ def test_populate_username_in_blacklist(db, settings, request_factory):
settings.SOCIALACCOUNT_AUTO_SIGNUP = True
request = request_factory.get("/accounts/twitter/login/callback/")
request.user = AnonymousUser()

User = get_user_model()
user = User()
setattr(user, account_settings.USER_MODEL_USERNAME_FIELD, "username")
setattr(
user,
account_settings.USER_MODEL_EMAIL_FIELD,
"username@example.com",
sociallogin = sociallogin_factory(
provider="twitter", username="username", email="username@example.com"
)

account = SocialAccount(provider="twitter", uid="123")
sociallogin = SocialLogin(user=user, account=account)
with context.request_context(request):
complete_social_login(request, sociallogin)

Expand Down

0 comments on commit cbd539c

Please sign in to comment.