Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into send_notification_e…
Browse files Browse the repository at this point in the history
…mail
  • Loading branch information
varun kumar committed Dec 28, 2023
2 parents ba4dd0a + 4982485 commit 237aedf
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 36 deletions.
8 changes: 8 additions & 0 deletions ChangeLog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Note worthy changes
change/set via the newly introduced ``get_password_change_redirect_url()``
adapter method.

- You can now configure the primary key of all models by configuring
``ALLAUTH_DEFAULT_AUTO_FIELD``, for example to:
``"hashid_field.HashidAutoField"``.


Backwards incompatible changes
------------------------------
Expand All @@ -20,6 +24,10 @@ Backwards incompatible changes
``foo`` uses ``/accounts/oidc/foo/login/`` as its login URL. Set it to empty
(``""``) to keep the previous URL structure (``/accounts/foo/login/``).

- The SAML default attribute mapping for ``uid`` has been changed to only
include ``urn:oasis:names:tc:SAML:attribute:subject-id``. If the SAML response
does not contain that, it will fallback to use ``NameID``.


0.59.0 (2023-12-13)
*******************
Expand Down
4 changes: 3 additions & 1 deletion allauth/account/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _

from allauth import app_settings


class AccountConfig(AppConfig):
name = "allauth.account"
verbose_name = _("Accounts")
default_auto_field = "django.db.models.AutoField"
default_auto_field = app_settings.DEFAULT_AUTO_FIELD or "django.db.models.AutoField"

def ready(self):
required_mw = "allauth.account.middleware.AccountMiddleware"
Expand Down
4 changes: 4 additions & 0 deletions allauth/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ def MFA_ENABLED(self):
def USERSESSIONS_ENABLED(self):
return apps.is_installed("allauth.usersessions")

@property
def DEFAULT_AUTO_FIELD(self):
return self._setting("DEFAULT_AUTO_FIELD", None)


_app_settings = AppSettings("ALLAUTH_")

Expand Down
6 changes: 5 additions & 1 deletion allauth/mfa/apps.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _

from allauth import app_settings


class MFAConfig(AppConfig):
name = "allauth.mfa"
verbose_name = _("MFA")
default_auto_field = "django.db.models.BigAutoField"
default_auto_field = (
app_settings.DEFAULT_AUTO_FIELD or "django.db.models.BigAutoField"
)

def ready(self):
from allauth.account import signals as account_signals
Expand Down
44 changes: 44 additions & 0 deletions allauth/socialaccount/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,50 @@ def get_requests_session(self):
)
return session

def is_email_verified(self, provider, email):
"""
Returns ``True`` iff the given email encountered during a social
login for the given provider is to be assumed verified.
This can be configured with a ``"verified_email"`` key in the provider
app settings, or a ``"VERIFIED_EMAIL"`` in the global provider settings
(``SOCIALACCOUNT_PROVIDERS``). Both can be set to ``False`` or
``True``, or, a list of domains to match email addresses against.
"""
verified_email = None
if provider.app:
verified_email = provider.app.settings.get("verified_email")
if verified_email is None:
settings = provider.get_settings()
verified_email = settings.get("VERIFIED_EMAIL", False)
if isinstance(verified_email, bool):
pass
elif isinstance(verified_email, list):
email_domain = email.partition("@")[2].lower()
verified_domains = [d.lower() for d in verified_email]
verified_email = email_domain in verified_domains
else:
raise ImproperlyConfigured("verified_email wrongly configured")
return verified_email

def can_authenticate_by_email(self, login, email):
"""
Returns ``True`` iff authentication by email is active for this login/email.
This can be configured with a ``"email_authentication"`` key in the provider
app settings, or a ``"VERIFIED_EMAIL"`` in the global provider settings
(``SOCIALACCOUNT_PROVIDERS``).
"""
ret = None
provider = login.account.get_provider()
if provider.app:
ret = provider.app.settings.get("email_authentication")
if ret is None:
ret = app_settings.EMAIL_AUTHENTICATION or provider.get_settings().get(
"EMAIL_AUTHENTICATION", False
)
return ret

def send_notification_mail(self, *args, **kwargs):
return get_account_adapter().send_notification_mail(*args, **kwargs)

Expand Down
4 changes: 3 additions & 1 deletion allauth/socialaccount/apps.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _

from allauth import app_settings


class SocialAccountConfig(AppConfig):
name = "allauth.socialaccount"
verbose_name = _("Social Accounts")
default_auto_field = "django.db.models.AutoField"
default_auto_field = app_settings.DEFAULT_AUTO_FIELD or "django.db.models.AutoField"
8 changes: 3 additions & 5 deletions allauth/socialaccount/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,7 @@ def lookup(self):
points, if any.
"""
if not self._lookup_by_socialaccount():
provider_id = self.account.get_provider().id
if app_settings.EMAIL_AUTHENTICATION or app_settings.PROVIDERS.get(
provider_id, {}
).get("EMAIL_AUTHENTICATION", False):
self._lookup_by_email()
self._lookup_by_email()

def _lookup_by_socialaccount(self):
assert not self.is_existing
Expand Down Expand Up @@ -328,6 +324,8 @@ def _lookup_by_socialaccount(self):
def _lookup_by_email(self):
emails = [e.email for e in self.email_addresses if e.verified]
for email in emails:
if not get_adapter().can_authenticate_by_email(self, email):
continue
users = filter_users_by_email(email, prefer_verified=True)
if users:
self.user = users[0]
Expand Down
10 changes: 6 additions & 4 deletions allauth/socialaccount/providers/base/provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.exceptions import ImproperlyConfigured

from allauth.socialaccount import app_settings
from allauth.socialaccount.adapter import get_adapter


class ProviderException(Exception):
Expand Down Expand Up @@ -68,6 +69,8 @@ def sociallogin_from_response(self, request, response):
raise ImproperlyConfigured(
f"SOCIALACCOUNT_UID_MAX_LENGTH too small (<{len(uid)})"
)
if not uid:
raise ValueError("uid must be a non-empty string")

extra_data = self.extract_extra_data(response)
common_fields = self.extract_common_fields(response)
Expand Down Expand Up @@ -133,10 +136,9 @@ def cleanup_email_addresses(self, email, addresses, email_verified=False):
EmailAddress(email=email, verified=bool(email_verified), primary=True)
)
# Force verified emails
settings = self.get_settings()
verified_email = settings.get("VERIFIED_EMAIL", False)
if verified_email:
for address in addresses:
adapter = get_adapter()
for address in addresses:
if adapter.is_email_verified(self, address.email):
address.verified = True

def extract_email_addresses(self, data):
Expand Down
44 changes: 37 additions & 7 deletions allauth/socialaccount/providers/saml/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class SAMLProvider(Provider):
account_class = SAMLAccount
default_attribute_mapping = {
"uid": [
"http://schemas.auth0.com/clientID",
"urn:oasis:names:tc:SAML:attribute:subject-id",
],
"email": [
Expand Down Expand Up @@ -51,11 +50,33 @@ def extract_extra_data(self, data):
return data.get_attributes()

def extract_uid(self, data):
"""http://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/csprd01/saml-subject-id-attr-v1.0-csprd01.html
Quotes:
"While the Attributes defined in this profile have as a goal the
explicit replacement of the <saml:NameID> element as a means of subject
identification, it is certainly possible to compose them with existing
NameID usage provided the same subject is being identified. This can
also serve as a migration strategy for existing applications."
"SAML does not define an identifier that meets all of these
requirements well. It does standardize a kind of NameID termed
“persistent” that meets some of them in the particular case of so-called
“pairwise” identification, where an identifier varies by relying
party. It has seen minimal adoption outside of a few contexts, and fails
at the “compact” and “simple to handle” criteria above, on top of the
disadvantages inherent with all NameID usage."
Overall, our strategy is to prefer a uid resulting from explicit
attribute mappings, and only if there is no such uid fallback to the
NameID.
"""
The `uid` is not unique across different SAML IdP's. Therefore,
we're using a fully qualified ID: <uid>@<entity_id>.
"""
return self._extract(data)["uid"]
uid = self._extract(data).get("uid")
if uid is None:
uid = data.get_nameid()
return uid

def extract_common_fields(self, data):
ret = self._extract(data)
Expand All @@ -74,14 +95,23 @@ def _extract(self, data):
if isinstance(provider_keys, str):
provider_keys = [provider_keys]
for provider_key in provider_keys:
attribute_list = raw_attributes.get(provider_key, [""])
if len(attribute_list) > 0:
attribute_list = raw_attributes.get(provider_key, None)
if attribute_list is not None and len(attribute_list) > 0:
attributes[key] = attribute_list[0]
break
email_verified = attributes.get("email_verified")
if email_verified:
email_verified = email_verified.lower() in ["true", "1", "t", "y", "yes"]
attributes["email_verified"] = email_verified

# If we did not find an email, check if the NameID contains the email.
if (
not attributes.get("email")
and data.get_nameid_format()
== "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
):
attributes["email"] = data.get_nameid()

return attributes


Expand Down
36 changes: 35 additions & 1 deletion allauth/socialaccount/providers/saml/tests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch
from unittest.mock import Mock, patch
from urllib.parse import parse_qs, urlparse

from django.urls import reverse
Expand All @@ -7,6 +7,7 @@
import pytest

from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers.saml.utils import build_saml_config

Expand Down Expand Up @@ -166,3 +167,36 @@ def test_build_saml_config(rf, provider_config):
assert config["idp"]["x509cert"] == "cert"
assert config["idp"]["singleSignOnService"] == {"url": "https://idp.org/sso/"}
assert config["idp"]["singleLogoutService"] == {"url": "https://idp.saml.org/slo/"}


@pytest.mark.parametrize(
"data, result, uid",
[
(
{"urn:oasis:names:tc:SAML:attribute:subject-id": ["123"]},
{"uid": "123", "email": "[email protected]"},
"123",
),
({}, {"email": "[email protected]"}, "[email protected]"),
],
)
def test_extract_attributes(db, data, result, uid, settings):
settings.SOCIALACCOUNT_PROVIDERS = {
"saml": {
"APPS": [
{
"client_id": "org",
"provider_id": "urn:dev-123.us.auth0.com",
}
]
}
}
provider = get_adapter().get_provider(request=None, provider="saml")
onelogin_data = Mock()
onelogin_data.get_attributes.return_value = data
onelogin_data.get_nameid.return_value = "[email protected]"
onelogin_data.get_nameid_format.return_value = (
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
)
assert provider._extract(onelogin_data) == result
assert provider.extract_uid(onelogin_data) == uid
25 changes: 11 additions & 14 deletions allauth/socialaccount/providers/sharefile/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ def get_mocked_response(self):
return MockedResponse(
200,
"""
{"access_token": "12345678abcdef",
"refresh_token": "12345678abcdef",
"token_type": "bearer",
"expires_in": 28800,
"appcp": "sharefile.com",
"apicp": "sharefile.com",
"subdomain": "example",
"access_files_folders": true,
"modify_files_folders": true,
"admin_users": true,
"admin_accounts": true,
"change_my_settings": true,
"web_app_login": true}
""",
{
"Id": "123",
"Email":"[email protected]",
"FirstName":"Name",
"LastName":"Last Name",
"Company":"Company",
"DefaultZone":
{
"Id":"zoneid"
}
} """,
)
6 changes: 5 additions & 1 deletion allauth/usersessions/apps.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _

from allauth import app_settings


class UserSessionsConfig(AppConfig):
name = "allauth.usersessions"
verbose_name = _("User Sessions")
default_auto_field = "django.db.models.BigAutoField"
default_auto_field = (
app_settings.DEFAULT_AUTO_FIELD or "django.db.models.BigAutoField"
)

def ready(self):
from allauth.account.signals import user_logged_in
Expand Down
8 changes: 8 additions & 0 deletions docs/common/configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Configuration
=============

Available settings:

``ALLAUTH_DEFAULT_AUTO_FIELD``
Can be set to configure the primary key of all models. For
example: ``"hashid_field.HashidAutoField"``.
1 change: 1 addition & 0 deletions docs/common/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Common Functionality
.. toctree::
:maxdepth: 1

configuration
email
templates
messages
Expand Down
2 changes: 1 addition & 1 deletion example/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ django-allauth example application in this directory:
$ cd django-allauth/example
$ virtualenv venv
$ . venv/bin/activate
$ pip install ..
$ pip install ..[mfa,saml]

Now we need to create the database tables and an admin user.
Run the following and when prompted to create a superuser choose yes and
Expand Down

0 comments on commit 237aedf

Please sign in to comment.