This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
r"""
_ ___ __ __ .___________. __ __
/\| |/\ / \ | | | | | || | | |
\ ` ' / / ^ \ | | | | `---| |----`| |__| |
|_ _| / /_\ \ | | | | | | | __ |
/ , . \ / _____ \ | `--' | | | | | | |
\/|_|\//__/ \__\ \______/ |__| |__| |__|
"""
VERSION = (0, 57, 0, "final", 0)
__title__ = "django-allauth"
__version_info__ = VERSION
__version__ = ".".join(map(str, VERSION[:3])) + (
"-{}{}".format(VERSION[3], VERSION[4] or "") if VERSION[3] != "final" else ""
)
__author__ = "Raymond Penners"
__license__ = "MIT"
__copyright__ = "Copyright 2010-2023 Raymond Penners and contributors"

View File

@@ -0,0 +1,681 @@
import html
import json
import warnings
from datetime import timedelta
from urllib.parse import urlparse
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
authenticate,
get_backends,
get_user_model,
login as django_login,
logout as django_logout,
)
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.password_validation import validate_password
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import FieldDoesNotExist
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import resolve_url
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from allauth import app_settings as allauth_app_settings
from allauth.account import signals
from allauth.account.app_settings import EmailVerificationMethod
from allauth.core import context, ratelimit
from allauth.utils import (
build_absolute_uri,
generate_unique_username,
import_attribute,
)
from . import app_settings
class DefaultAccountAdapter(object):
error_messages = {
"username_blacklisted": _(
"Username can not be used. Please use other username."
),
"username_taken": AbstractUser._meta.get_field("username").error_messages[
"unique"
],
"too_many_login_attempts": _(
"Too many failed login attempts. Try again later."
),
"email_taken": _("A user is already registered with this email address."),
"incorrect_password": _("Incorrect password."),
}
def __init__(self, request=None):
# Explicitly passing `request` is deprecated, just use:
# `allauth.core.context.request`.
self.request = context.request
def stash_verified_email(self, request, email):
request.session["account_verified_email"] = email
def unstash_verified_email(self, request):
ret = request.session.get("account_verified_email")
request.session["account_verified_email"] = None
return ret
def stash_user(self, request, user):
request.session["account_user"] = user
def unstash_user(self, request):
return request.session.pop("account_user", None)
def is_email_verified(self, request, email):
"""
Checks whether or not the email address is already verified
beyond allauth scope, for example, by having accepted an
invitation before signing up.
"""
ret = False
verified_email = request.session.get("account_verified_email")
if verified_email:
ret = verified_email.lower() == email.lower()
return ret
def format_email_subject(self, subject):
prefix = app_settings.EMAIL_SUBJECT_PREFIX
if prefix is None:
site = get_current_site(context.request)
prefix = "[{name}] ".format(name=site.name)
return prefix + force_str(subject)
def get_from_email(self):
"""
This is a hook that can be overridden to programmatically
set the 'from' email address for sending emails
"""
return settings.DEFAULT_FROM_EMAIL
def render_mail(self, template_prefix, email, context, headers=None):
"""
Renders an email to `email`. `template_prefix` identifies the
email that is to be sent, e.g. "account/email/email_confirmation"
"""
to = [email] if isinstance(email, str) else email
subject = render_to_string("{0}_subject.txt".format(template_prefix), context)
# remove superfluous line breaks
subject = " ".join(subject.splitlines()).strip()
subject = self.format_email_subject(subject)
from_email = self.get_from_email()
bodies = {}
for ext in ["html", "txt"]:
try:
template_name = "{0}_message.{1}".format(template_prefix, ext)
bodies[ext] = render_to_string(
template_name,
context,
globals()["context"].request,
).strip()
except TemplateDoesNotExist:
if ext == "txt" and not bodies:
# We need at least one body
raise
if "txt" in bodies:
msg = EmailMultiAlternatives(
subject, bodies["txt"], from_email, to, headers=headers
)
if "html" in bodies:
msg.attach_alternative(bodies["html"], "text/html")
else:
msg = EmailMessage(subject, bodies["html"], from_email, to, headers=headers)
msg.content_subtype = "html" # Main content is now text/html
return msg
def send_mail(self, template_prefix, email, context):
msg = self.render_mail(template_prefix, email, context)
msg.send()
def get_signup_redirect_url(self, request):
return resolve_url(app_settings.SIGNUP_REDIRECT_URL)
def get_login_redirect_url(self, request):
"""
Returns the default URL to redirect to after logging in. Note
that URLs passed explicitly (e.g. by passing along a `next`
GET parameter) take precedence over the value returned here.
"""
assert request.user.is_authenticated
url = getattr(settings, "LOGIN_REDIRECT_URLNAME", None)
if url:
warnings.warn(
"LOGIN_REDIRECT_URLNAME is deprecated, simply"
" use LOGIN_REDIRECT_URL with a URL name",
DeprecationWarning,
)
else:
url = settings.LOGIN_REDIRECT_URL
return resolve_url(url)
def get_logout_redirect_url(self, request):
"""
Returns the URL to redirect to after the user logs out. Note that
this method is also invoked if you attempt to log out while no users
is logged in. Therefore, request.user is not guaranteed to be an
authenticated user.
"""
return resolve_url(app_settings.LOGOUT_REDIRECT_URL)
def get_email_confirmation_redirect_url(self, request):
"""
The URL to return to after successful email confirmation.
"""
if request.user.is_authenticated:
if app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL:
return app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL
else:
return self.get_login_redirect_url(request)
else:
return app_settings.EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL
def is_open_for_signup(self, request):
"""
Checks whether or not the site is open for signups.
Next to simply returning True/False you can also intervene the
regular flow by raising an ImmediateHttpResponse
"""
return True
def new_user(self, request):
"""
Instantiates a new User instance.
"""
user = get_user_model()()
return user
def populate_username(self, request, user):
"""
Fills in a valid username, if required and missing. If the
username is already present it is assumed to be valid
(unique).
"""
from .utils import user_email, user_field, user_username
first_name = user_field(user, "first_name")
last_name = user_field(user, "last_name")
email = user_email(user)
username = user_username(user)
if app_settings.USER_MODEL_USERNAME_FIELD:
user_username(
user,
username
or self.generate_unique_username(
[first_name, last_name, email, username, "user"]
),
)
def generate_unique_username(self, txts, regex=None):
return generate_unique_username(txts, regex)
def save_user(self, request, user, form, commit=True):
"""
Saves a new `User` instance using information provided in the
signup form.
"""
from .utils import user_email, user_field, user_username
data = form.cleaned_data
first_name = data.get("first_name")
last_name = data.get("last_name")
email = data.get("email")
username = data.get("username")
user_email(user, email)
user_username(user, username)
if first_name:
user_field(user, "first_name", first_name)
if last_name:
user_field(user, "last_name", last_name)
if "password1" in data:
user.set_password(data["password1"])
else:
user.set_unusable_password()
self.populate_username(request, user)
if commit:
# Ability not to commit makes it easier to derive from
# this adapter by adding
user.save()
return user
def clean_username(self, username, shallow=False):
"""
Validates the username. You can hook into this if you want to
(dynamically) restrict what usernames can be chosen.
"""
for validator in app_settings.USERNAME_VALIDATORS:
validator(username)
# TODO: Add regexp support to USERNAME_BLACKLIST
username_blacklist_lower = [
ub.lower() for ub in app_settings.USERNAME_BLACKLIST
]
if username.lower() in username_blacklist_lower:
raise forms.ValidationError(self.error_messages["username_blacklisted"])
# Skipping database lookups when shallow is True, needed for unique
# username generation.
if not shallow:
from .utils import filter_users_by_username
if filter_users_by_username(username).exists():
user_model = get_user_model()
username_field = app_settings.USER_MODEL_USERNAME_FIELD
error_message = user_model._meta.get_field(
username_field
).error_messages.get("unique")
if not error_message:
error_message = self.error_messages["username_taken"]
raise forms.ValidationError(
error_message,
params={
"model_name": user_model.__name__,
"field_label": username_field,
},
)
return username
def clean_email(self, email):
"""
Validates an email value. You can hook into this if you want to
(dynamically) restrict what email addresses can be chosen.
"""
return email
def clean_password(self, password, user=None):
"""
Validates a password. You can hook into this if you want to
restric the allowed password choices.
"""
min_length = app_settings.PASSWORD_MIN_LENGTH
if min_length and len(password) < min_length:
raise forms.ValidationError(
_("Password must be a minimum of {0} characters.").format(min_length)
)
validate_password(password, user)
return password
def validate_unique_email(self, email):
return email
def add_message(
self,
request,
level,
message_template,
message_context=None,
extra_tags="",
):
"""
Wrapper of `django.contrib.messages.add_message`, that reads
the message text from a template.
"""
if "django.contrib.messages" in settings.INSTALLED_APPS:
try:
if message_context is None:
message_context = {}
escaped_message = render_to_string(
message_template,
message_context,
context.request,
).strip()
if escaped_message:
message = html.unescape(escaped_message)
messages.add_message(request, level, message, extra_tags=extra_tags)
except TemplateDoesNotExist:
pass
def ajax_response(self, request, response, redirect_to=None, form=None, data=None):
resp = {}
status = response.status_code
if redirect_to:
status = 200
resp["location"] = redirect_to
if form:
if request.method == "POST":
if form.is_valid():
status = 200
else:
status = 400
else:
status = 200
resp["form"] = self.ajax_response_form(form)
if hasattr(response, "render"):
response.render()
resp["html"] = response.content.decode("utf8")
if data is not None:
resp["data"] = data
return HttpResponse(
json.dumps(resp), status=status, content_type="application/json"
)
def ajax_response_form(self, form):
form_spec = {
"fields": {},
"field_order": [],
"errors": form.non_field_errors(),
}
for field in form:
field_spec = {
"label": force_str(field.label),
"value": field.value(),
"help_text": force_str(field.help_text),
"errors": [force_str(e) for e in field.errors],
"widget": {
"attrs": {
k: force_str(v) for k, v in field.field.widget.attrs.items()
}
},
}
form_spec["fields"][field.html_name] = field_spec
form_spec["field_order"].append(field.html_name)
return form_spec
def pre_login(
self,
request,
user,
*,
email_verification,
signal_kwargs,
email,
signup,
redirect_url
):
from .utils import has_verified_email, send_email_confirmation
if not user.is_active:
return self.respond_user_inactive(request, user)
if email_verification == EmailVerificationMethod.NONE:
pass
elif email_verification == EmailVerificationMethod.OPTIONAL:
# In case of OPTIONAL verification: send on signup.
if not has_verified_email(user, email) and signup:
send_email_confirmation(request, user, signup=signup, email=email)
elif email_verification == EmailVerificationMethod.MANDATORY:
if not has_verified_email(user, email):
send_email_confirmation(request, user, signup=signup, email=email)
return self.respond_email_verification_sent(request, user)
def post_login(
self,
request,
user,
*,
email_verification,
signal_kwargs,
email,
signup,
redirect_url
):
from .utils import get_login_redirect_url
response = HttpResponseRedirect(
get_login_redirect_url(request, redirect_url, signup=signup)
)
if signal_kwargs is None:
signal_kwargs = {}
signals.user_logged_in.send(
sender=user.__class__,
request=request,
response=response,
user=user,
**signal_kwargs,
)
self.add_message(
request,
messages.SUCCESS,
"account/messages/logged_in.txt",
{"user": user},
)
return response
def login(self, request, user):
from allauth.account.utils import record_authentication
# HACK: This is not nice. The proper Django way is to use an
# authentication backend
if not hasattr(user, "backend"):
from .auth_backends import AuthenticationBackend
backends = get_backends()
backend = None
for b in backends:
if isinstance(b, AuthenticationBackend):
# prefer our own backend
backend = b
break
elif not backend and hasattr(b, "get_user"):
# Pick the first valid one
backend = b
backend_path = ".".join([backend.__module__, backend.__class__.__name__])
user.backend = backend_path
django_login(request, user)
record_authentication(request, user)
def logout(self, request):
django_logout(request)
def confirm_email(self, request, email_address):
"""
Marks the email address as confirmed on the db
"""
from allauth.account.models import EmailAddress
from_email_address = (
EmailAddress.objects.filter(user_id=email_address.user_id)
.exclude(pk=email_address.pk)
.first()
)
if not email_address.set_verified(commit=False):
return False
email_address.set_as_primary(conditional=(not app_settings.CHANGE_EMAIL))
email_address.save(update_fields=["verified", "primary"])
if app_settings.CHANGE_EMAIL:
for instance in EmailAddress.objects.filter(
user_id=email_address.user_id
).exclude(pk=email_address.pk):
instance.remove()
signals.email_changed.send(
sender=get_user_model(),
request=request,
user=email_address.user,
from_email_address=from_email_address,
to_email_address=email_address,
)
return True
def set_password(self, user, password):
user.set_password(password)
user.save()
def get_user_search_fields(self):
ret = []
User = get_user_model()
candidates = [
app_settings.USER_MODEL_USERNAME_FIELD,
"first_name",
"last_name",
"email",
]
for candidate in candidates:
try:
User._meta.get_field(candidate)
ret.append(candidate)
except FieldDoesNotExist:
pass
return ret
def is_safe_url(self, url):
try:
from django.utils.http import url_has_allowed_host_and_scheme
except ImportError:
from django.utils.http import (
is_safe_url as url_has_allowed_host_and_scheme,
)
# get_host already validates the given host, so no need to check it again
allowed_hosts = {context.request.get_host()} | set(settings.ALLOWED_HOSTS)
if "*" in allowed_hosts:
parsed_host = urlparse(url).netloc
allowed_host = {parsed_host} if parsed_host else None
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_host)
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)
def get_email_confirmation_url(self, request, emailconfirmation):
"""Constructs the email confirmation (activation) url.
Note that if you have architected your system such that email
confirmations are sent outside of the request context `request`
can be `None` here.
"""
url = reverse("account_confirm_email", args=[emailconfirmation.key])
ret = build_absolute_uri(request, url)
return ret
def should_send_confirmation_mail(self, request, email_address):
from allauth.account.models import EmailConfirmation
cooldown_period = timedelta(seconds=app_settings.EMAIL_CONFIRMATION_COOLDOWN)
if app_settings.EMAIL_CONFIRMATION_HMAC:
send_email = ratelimit.consume(
request,
action="confirm_email",
key=email_address.email.lower(),
amount=1,
duration=cooldown_period.total_seconds(),
)
else:
send_email = not EmailConfirmation.objects.filter(
sent__gt=timezone.now() - cooldown_period,
email_address=email_address,
).exists()
return send_email
def send_account_already_exists_mail(self, email):
signup_url = build_absolute_uri(context.request, reverse("account_signup"))
password_reset_url = build_absolute_uri(
context.request, reverse("account_reset_password")
)
ctx = {
"request": context.request,
"current_site": get_current_site(context.request),
"email": email,
"signup_url": signup_url,
"password_reset_url": password_reset_url,
}
self.send_mail("account/email/account_already_exists", email, ctx)
def send_confirmation_mail(self, request, emailconfirmation, signup):
current_site = get_current_site(request)
activate_url = self.get_email_confirmation_url(request, emailconfirmation)
ctx = {
"user": emailconfirmation.email_address.user,
"activate_url": activate_url,
"current_site": current_site,
"key": emailconfirmation.key,
}
if signup:
email_template = "account/email/email_confirmation_signup"
else:
email_template = "account/email/email_confirmation"
self.send_mail(email_template, emailconfirmation.email_address.email, ctx)
def respond_user_inactive(self, request, user):
return HttpResponseRedirect(reverse("account_inactive"))
def respond_email_verification_sent(self, request, user):
return HttpResponseRedirect(reverse("account_email_verification_sent"))
def _get_login_attempts_cache_key(self, request, **credentials):
site = get_current_site(request)
login = credentials.get("email", credentials.get("username", "")).lower()
return "{site}:{login}".format(site=site.domain, login=login)
def _delete_login_attempts_cached_email(self, request, **credentials):
if app_settings.LOGIN_ATTEMPTS_LIMIT:
cache_key = self._get_login_attempts_cache_key(request, **credentials)
ratelimit.clear(request, action="login_failed", key=cache_key)
def pre_authenticate(self, request, **credentials):
if app_settings.LOGIN_ATTEMPTS_LIMIT:
cache_key = self._get_login_attempts_cache_key(request, **credentials)
if not ratelimit.consume(
request,
action="login_failed",
key=cache_key,
amount=app_settings.LOGIN_ATTEMPTS_LIMIT,
duration=app_settings.LOGIN_ATTEMPTS_TIMEOUT,
):
raise forms.ValidationError(
self.error_messages["too_many_login_attempts"]
)
def authenticate(self, request, **credentials):
"""Only authenticates, does not actually login. See `login`"""
from allauth.account.auth_backends import AuthenticationBackend
self.pre_authenticate(request, **credentials)
AuthenticationBackend.unstash_authenticated_user()
user = authenticate(request, **credentials)
alt_user = AuthenticationBackend.unstash_authenticated_user()
user = user or alt_user
if user and app_settings.LOGIN_ATTEMPTS_LIMIT:
self._delete_login_attempts_cached_email(request, **credentials)
else:
self.authentication_failed(request, **credentials)
return user
def authentication_failed(self, request, **credentials):
pass
def is_ajax(self, request):
return any(
[
request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest",
request.content_type == "application/json",
request.META.get("HTTP_ACCEPT") == "application/json",
]
)
def get_client_ip(self, request):
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0]
else:
ip = request.META.get("REMOTE_ADDR")
return ip
def generate_emailconfirmation_key(self, email):
key = get_random_string(64).lower()
return key
def get_login_stages(self):
ret = []
if allauth_app_settings.MFA_ENABLED:
ret.append("allauth.mfa.stages.AuthenticateStage")
return ret
def get_adapter(request=None):
return import_attribute(app_settings.ADAPTER)(request)

View File

@@ -0,0 +1,33 @@
from django.contrib import admin
from . import app_settings
from .adapter import get_adapter
from .models import EmailAddress, EmailConfirmation
class EmailAddressAdmin(admin.ModelAdmin):
list_display = ("email", "user", "primary", "verified")
list_filter = ("primary", "verified")
search_fields = []
raw_id_fields = ("user",)
actions = ["make_verified"]
def get_search_fields(self, request):
base_fields = get_adapter().get_user_search_fields()
return ["email"] + list(map(lambda a: "user__" + a, base_fields))
def make_verified(self, request, queryset):
queryset.update(verified=True)
make_verified.short_description = "Mark selected email addresses as verified"
class EmailConfirmationAdmin(admin.ModelAdmin):
list_display = ("email_address", "created", "sent", "key")
list_filter = ("sent",)
raw_id_fields = ("email_address",)
if not app_settings.EMAIL_CONFIRMATION_HMAC:
admin.site.register(EmailConfirmation, EmailConfirmationAdmin)
admin.site.register(EmailAddress, EmailAddressAdmin)

View File

@@ -0,0 +1,399 @@
from django.core.exceptions import ImproperlyConfigured
class AppSettings(object):
class AuthenticationMethod:
USERNAME = "username"
EMAIL = "email"
USERNAME_EMAIL = "username_email"
class EmailVerificationMethod:
# After signing up, keep the user account inactive until the email
# address is verified
MANDATORY = "mandatory"
# Allow login with unverified email (email verification is
# still sent)
OPTIONAL = "optional"
# Don't send email verification mails during signup
NONE = "none"
def __init__(self, prefix):
self.prefix = prefix
# If login is by email, email must be required
assert (
not self.AUTHENTICATION_METHOD == self.AuthenticationMethod.EMAIL
) or self.EMAIL_REQUIRED
# If login includes email, login must be unique
assert (
self.AUTHENTICATION_METHOD == self.AuthenticationMethod.USERNAME
) or self.UNIQUE_EMAIL
assert (
self.EMAIL_VERIFICATION != self.EmailVerificationMethod.MANDATORY
) or self.EMAIL_REQUIRED
if not self.USER_MODEL_USERNAME_FIELD:
assert not self.USERNAME_REQUIRED
assert self.AUTHENTICATION_METHOD not in (
self.AuthenticationMethod.USERNAME,
self.AuthenticationMethod.USERNAME_EMAIL,
)
if self.MAX_EMAIL_ADDRESSES is not None:
assert self.MAX_EMAIL_ADDRESSES > 0
if self.CHANGE_EMAIL:
if self.MAX_EMAIL_ADDRESSES is not None and self.MAX_EMAIL_ADDRESSES != 2:
raise ImproperlyConfigured(
"Invalid combination of ACCOUNT_CHANGE_EMAIL and ACCOUNT_MAX_EMAIL_ADDRESSES"
)
def _setting(self, name, dflt):
from allauth.utils import get_setting
return get_setting(self.prefix + name, dflt)
@property
def PREVENT_ENUMERATION(self):
return self._setting("PREVENT_ENUMERATION", True)
@property
def DEFAULT_HTTP_PROTOCOL(self):
return self._setting("DEFAULT_HTTP_PROTOCOL", "http").lower()
@property
def EMAIL_CONFIRMATION_EXPIRE_DAYS(self):
"""
Determines the expiration date of email confirmation mails (#
of days)
"""
from django.conf import settings
return self._setting(
"EMAIL_CONFIRMATION_EXPIRE_DAYS",
getattr(settings, "EMAIL_CONFIRMATION_DAYS", 3),
)
@property
def EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL(self):
"""
The URL to redirect to after a successful email confirmation, in
case of an authenticated user
"""
return self._setting("EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL", None)
@property
def EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL(self):
"""
The URL to redirect to after a successful email confirmation, in
case no user is logged in
"""
from django.conf import settings
return self._setting(
"EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL", settings.LOGIN_URL
)
@property
def EMAIL_CONFIRMATION_COOLDOWN(self):
"""
The cooldown in seconds during which, after an email confirmation has
been sent, a second confirmation email will not be sent.
"""
return self._setting("EMAIL_CONFIRMATION_COOLDOWN", 3 * 60)
@property
def EMAIL_REQUIRED(self):
"""
The user is required to hand over an email address when signing up
"""
return self._setting("EMAIL_REQUIRED", False)
@property
def EMAIL_VERIFICATION(self):
"""
See email verification method
"""
ret = self._setting("EMAIL_VERIFICATION", self.EmailVerificationMethod.OPTIONAL)
# Deal with legacy (boolean based) setting
if ret is True:
ret = self.EmailVerificationMethod.MANDATORY
elif ret is False:
ret = self.EmailVerificationMethod.OPTIONAL
return ret
@property
def MAX_EMAIL_ADDRESSES(self):
return self._setting("MAX_EMAIL_ADDRESSES", None)
@property
def CHANGE_EMAIL(self):
return self._setting("CHANGE_EMAIL", False)
@property
def AUTHENTICATION_METHOD(self):
ret = self._setting("AUTHENTICATION_METHOD", self.AuthenticationMethod.USERNAME)
return ret
@property
def EMAIL_MAX_LENGTH(self):
"""
Adjust max_length of email addresses
"""
return self._setting("EMAIL_MAX_LENGTH", 254)
@property
def UNIQUE_EMAIL(self):
"""
Enforce uniqueness of email addresses
"""
return self._setting("UNIQUE_EMAIL", True)
@property
def SIGNUP_EMAIL_ENTER_TWICE(self):
"""
Signup email verification
"""
return self._setting("SIGNUP_EMAIL_ENTER_TWICE", False)
@property
def SIGNUP_PASSWORD_ENTER_TWICE(self):
"""
Signup password verification
"""
legacy = self._setting("SIGNUP_PASSWORD_VERIFICATION", True)
return self._setting("SIGNUP_PASSWORD_ENTER_TWICE", legacy)
@property
def SIGNUP_REDIRECT_URL(self):
from django.conf import settings
return self._setting("SIGNUP_REDIRECT_URL", settings.LOGIN_REDIRECT_URL)
@property
def PASSWORD_MIN_LENGTH(self):
"""
Minimum password Length
"""
from django.conf import settings
ret = None
if not settings.AUTH_PASSWORD_VALIDATORS:
ret = self._setting("PASSWORD_MIN_LENGTH", 6)
return ret
@property
def RATE_LIMITS(self):
dflt = {
# Change password view (for users already logged in)
"change_password": "5/m",
# Email management (e.g. add, remove, change primary)
"manage_email": "10/m",
# Request a password reset, global rate limit per IP
"reset_password": "20/m",
# Rate limit measured per individual email address
"reset_password_email": "5/m",
# Reauthentication for users already logged in)
"reauthenticate": "10/m",
# Password reset (the view the password reset email links to).
"reset_password_from_key": "20/m",
# Signups.
"signup": "20/m",
# NOTE: Login is already protected via `ACCOUNT_LOGIN_ATTEMPTS_LIMIT`
}
return self._setting("RATE_LIMITS", dflt)
@property
def EMAIL_SUBJECT_PREFIX(self):
"""
Subject-line prefix to use for email messages sent
"""
return self._setting("EMAIL_SUBJECT_PREFIX", None)
@property
def SIGNUP_FORM_CLASS(self):
"""
Signup form
"""
return self._setting("SIGNUP_FORM_CLASS", None)
@property
def USERNAME_REQUIRED(self):
"""
The user is required to enter a username when signing up
"""
return self._setting("USERNAME_REQUIRED", True)
@property
def USERNAME_MIN_LENGTH(self):
"""
Minimum username Length
"""
return self._setting("USERNAME_MIN_LENGTH", 1)
@property
def USERNAME_BLACKLIST(self):
"""
List of usernames that are not allowed
"""
return self._setting("USERNAME_BLACKLIST", [])
@property
def PASSWORD_INPUT_RENDER_VALUE(self):
"""
render_value parameter as passed to PasswordInput fields
"""
return self._setting("PASSWORD_INPUT_RENDER_VALUE", False)
@property
def ADAPTER(self):
return self._setting("ADAPTER", "allauth.account.adapter.DefaultAccountAdapter")
@property
def CONFIRM_EMAIL_ON_GET(self):
return self._setting("CONFIRM_EMAIL_ON_GET", False)
@property
def AUTHENTICATED_LOGIN_REDIRECTS(self):
return self._setting("AUTHENTICATED_LOGIN_REDIRECTS", True)
@property
def LOGIN_ON_EMAIL_CONFIRMATION(self):
"""
Automatically log the user in once they confirmed their email address
"""
return self._setting("LOGIN_ON_EMAIL_CONFIRMATION", False)
@property
def LOGIN_ON_PASSWORD_RESET(self):
"""
Automatically log the user in immediately after resetting
their password.
"""
return self._setting("LOGIN_ON_PASSWORD_RESET", False)
@property
def LOGOUT_REDIRECT_URL(self):
from django.conf import settings
return self._setting("LOGOUT_REDIRECT_URL", settings.LOGOUT_REDIRECT_URL or "/")
@property
def LOGOUT_ON_GET(self):
return self._setting("LOGOUT_ON_GET", False)
@property
def LOGOUT_ON_PASSWORD_CHANGE(self):
return self._setting("LOGOUT_ON_PASSWORD_CHANGE", False)
@property
def USER_MODEL_USERNAME_FIELD(self):
return self._setting("USER_MODEL_USERNAME_FIELD", "username")
@property
def USER_MODEL_EMAIL_FIELD(self):
return self._setting("USER_MODEL_EMAIL_FIELD", "email")
@property
def SESSION_COOKIE_AGE(self):
"""
Deprecated -- use Django's settings.SESSION_COOKIE_AGE instead
"""
from django.conf import settings
return self._setting("SESSION_COOKIE_AGE", settings.SESSION_COOKIE_AGE)
@property
def SESSION_REMEMBER(self):
"""
Controls the life time of the session. Set to `None` to ask the user
("Remember me?"), `False` to not remember, and `True` to always
remember.
"""
return self._setting("SESSION_REMEMBER", None)
@property
def TEMPLATE_EXTENSION(self):
"""
A string defining the template extension to use, defaults to `html`.
"""
return self._setting("TEMPLATE_EXTENSION", "html")
@property
def FORMS(self):
return self._setting("FORMS", {})
@property
def LOGIN_ATTEMPTS_LIMIT(self):
"""
Number of failed login attempts. When this number is
exceeded, the user is prohibited from logging in for the
specified `LOGIN_ATTEMPTS_TIMEOUT`
"""
return self._setting("LOGIN_ATTEMPTS_LIMIT", 5)
@property
def LOGIN_ATTEMPTS_TIMEOUT(self):
"""
Time period from last unsuccessful login attempt, during
which the user is prohibited from trying to log in. Defaults to
5 minutes.
"""
return self._setting("LOGIN_ATTEMPTS_TIMEOUT", 60 * 5)
@property
def EMAIL_CONFIRMATION_HMAC(self):
return self._setting("EMAIL_CONFIRMATION_HMAC", True)
@property
def SALT(self):
return self._setting("SALT", "account")
@property
def PRESERVE_USERNAME_CASING(self):
return self._setting("PRESERVE_USERNAME_CASING", True)
@property
def USERNAME_VALIDATORS(self):
from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from allauth.utils import import_attribute
path = self._setting("USERNAME_VALIDATORS", None)
if path:
ret = import_attribute(path)
if not isinstance(ret, list):
raise ImproperlyConfigured(
"ACCOUNT_USERNAME_VALIDATORS is expected to be a list"
)
else:
if self.USER_MODEL_USERNAME_FIELD is not None:
ret = (
get_user_model()
._meta.get_field(self.USER_MODEL_USERNAME_FIELD)
.validators
)
else:
ret = []
return ret
@property
def PASSWORD_RESET_TOKEN_GENERATOR(self):
from allauth.account.forms import EmailAwarePasswordResetTokenGenerator
from allauth.utils import import_attribute
token_generator_path = self._setting("PASSWORD_RESET_TOKEN_GENERATOR", None)
if token_generator_path is not None:
token_generator = import_attribute(token_generator_path)
else:
token_generator = EmailAwarePasswordResetTokenGenerator
return token_generator
@property
def REAUTHENTICATION_TIMEOUT(self):
return self._setting("REAUTHENTICATION_TIMEOUT", 300)
_app_settings = AppSettings("ACCOUNT_")
def __getattr__(name):
# See https://peps.python.org/pep-0562/
return getattr(_app_settings, name)

View File

@@ -0,0 +1,17 @@
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
class AccountConfig(AppConfig):
name = "allauth.account"
verbose_name = _("Accounts")
default_auto_field = "django.db.models.AutoField"
def ready(self):
required_mw = "allauth.account.middleware.AccountMiddleware"
if required_mw not in settings.MIDDLEWARE:
raise ImproperlyConfigured(
f"{required_mw} must be added to settings.MIDDLEWARE"
)

View File

@@ -0,0 +1,99 @@
from threading import local
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from . import app_settings
from .app_settings import AuthenticationMethod
from .utils import filter_users_by_email, filter_users_by_username
_stash = local()
class AuthenticationBackend(ModelBackend):
def authenticate(self, request, **credentials):
ret = None
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
ret = self._authenticate_by_email(**credentials)
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME_EMAIL:
ret = self._authenticate_by_email(**credentials)
if not ret:
ret = self._authenticate_by_username(**credentials)
else:
ret = self._authenticate_by_username(**credentials)
return ret
def _authenticate_by_username(self, **credentials):
username_field = app_settings.USER_MODEL_USERNAME_FIELD
username = credentials.get("username")
password = credentials.get("password")
User = get_user_model()
if not username_field or username is None or password is None:
return None
try:
# Username query is case insensitive
user = filter_users_by_username(username).get()
except User.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user.
get_user_model()().set_password(password)
return None
else:
if self._check_password(user, password):
return user
def _authenticate_by_email(self, **credentials):
# Even though allauth will pass along `email`, other apps may
# not respect this setting. For example, when using
# django-tastypie basic authentication, the login is always
# passed as `username`. So let's play nice with other apps
# and use username as fallback
email = credentials.get("email", credentials.get("username"))
if email:
for user in filter_users_by_email(email, prefer_verified=True):
if self._check_password(user, credentials["password"]):
return user
return None
def _check_password(self, user, password):
ret = user.check_password(password)
if ret:
ret = self.user_can_authenticate(user)
if not ret:
self._stash_user(user)
return ret
@classmethod
def _stash_user(cls, user):
"""Now, be aware, the following is quite ugly, let me explain:
Even if the user credentials match, the authentication can fail because
Django's default ModelBackend calls user_can_authenticate(), which
checks `is_active`. Now, earlier versions of allauth did not do this
and simply returned the user as authenticated, even in case of
`is_active=False`. For allauth scope, this does not pose a problem, as
these users are properly redirected to an account inactive page.
This does pose a problem when the allauth backend is used in a
different context where allauth is not responsible for the login. Then,
by not checking on `user_can_authenticate()` users will allow to become
authenticated whereas according to Django logic this should not be
allowed.
In order to preserve the allauth behavior while respecting Django's
logic, we stash a user for which the password check succeeded but
`user_can_authenticate()` failed. In the allauth authentication logic,
we can then unstash this user and proceed pointing the user to the
account inactive page.
"""
global _stash
ret = getattr(_stash, "user", None)
_stash.user = user
return ret
@classmethod
def unstash_authenticated_user(cls):
return cls._stash_user(None)

View File

@@ -0,0 +1,73 @@
from functools import wraps
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from django.utils.http import urlencode
from .models import EmailAddress
from .utils import did_recently_authenticate, send_email_confirmation
def verified_email_required(
function=None, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME
):
"""
Even when email verification is not mandatory during signup, there
may be circumstances during which you really want to prevent
unverified users to proceed. This decorator ensures the user is
authenticated and has a verified email address. If the former is
not the case then the behavior is identical to that of the
standard `login_required` decorator. If the latter does not hold,
email verification mails are automatically resend and the user is
presented with a page informing them they needs to verify their email
address.
"""
def decorator(view_func):
@login_required(redirect_field_name=redirect_field_name, login_url=login_url)
def _wrapped_view(request, *args, **kwargs):
if not EmailAddress.objects.filter(
user=request.user, verified=True
).exists():
send_email_confirmation(request, request.user)
return render(request, "account/verified_email_required.html")
return view_func(request, *args, **kwargs)
return _wrapped_view
if function:
return decorator(function)
return decorator
def reauthentication_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME):
def decorator(view_func):
@wraps(view_func)
def _wrapper_view(request, *args, **kwargs):
path = request.get_full_path()
if request.user.is_anonymous:
redirect_url = (
reverse("account_login")
+ "?"
+ urlencode({redirect_field_name: path})
)
return HttpResponseRedirect(redirect_url)
if not did_recently_authenticate(request):
redirect_url = (
reverse("account_reauthenticate")
+ "?"
+ urlencode({redirect_field_name: path})
)
return HttpResponseRedirect(redirect_url)
return view_func(request, *args, **kwargs)
return _wrapper_view
if function:
return decorator(function)
return decorator

View File

@@ -0,0 +1,687 @@
from __future__ import absolute_import
from importlib import import_module
from django import forms
from django.contrib.auth import password_validation
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.sites.shortcuts import get_current_site
from django.core import exceptions, validators
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _, pgettext
from ..utils import (
build_absolute_uri,
get_username_max_length,
set_form_field_order,
)
from . import app_settings
from .adapter import get_adapter
from .app_settings import AuthenticationMethod
from .models import EmailAddress
from .utils import (
assess_unique_email,
filter_users_by_email,
get_user_model,
perform_login,
setup_user_email,
sync_user_email_addresses,
url_str_to_user_pk,
user_email,
user_pk_to_url_str,
user_username,
)
class EmailAwarePasswordResetTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
ret = super(EmailAwarePasswordResetTokenGenerator, self)._make_hash_value(
user, timestamp
)
sync_user_email_addresses(user)
email = user_email(user)
emails = set([email] if email else [])
emails.update(
EmailAddress.objects.filter(user=user).values_list("email", flat=True)
)
ret += "|".join(sorted(emails))
return ret
default_token_generator = app_settings.PASSWORD_RESET_TOKEN_GENERATOR()
class PasswordVerificationMixin(object):
def clean(self):
cleaned_data = super(PasswordVerificationMixin, self).clean()
password1 = cleaned_data.get("password1")
password2 = cleaned_data.get("password2")
if (password1 and password2) and password1 != password2:
self.add_error("password2", _("You must type the same password each time."))
return cleaned_data
class PasswordField(forms.CharField):
def __init__(self, *args, **kwargs):
render_value = kwargs.pop(
"render_value", app_settings.PASSWORD_INPUT_RENDER_VALUE
)
kwargs["widget"] = forms.PasswordInput(
render_value=render_value,
attrs={"placeholder": kwargs.get("label")},
)
autocomplete = kwargs.pop("autocomplete", None)
if autocomplete is not None:
kwargs["widget"].attrs["autocomplete"] = autocomplete
super(PasswordField, self).__init__(*args, **kwargs)
class SetPasswordField(PasswordField):
def __init__(self, *args, **kwargs):
kwargs["autocomplete"] = "new-password"
super(SetPasswordField, self).__init__(*args, **kwargs)
self.user = None
def clean(self, value):
value = super(SetPasswordField, self).clean(value)
value = get_adapter().clean_password(value, user=self.user)
return value
class LoginForm(forms.Form):
password = PasswordField(label=_("Password"), autocomplete="current-password")
remember = forms.BooleanField(label=_("Remember Me"), required=False)
user = None
error_messages = {
"account_inactive": _("This account is currently inactive."),
"email_password_mismatch": _(
"The email address and/or password you specified are not correct."
),
"username_password_mismatch": _(
"The username and/or password you specified are not correct."
),
}
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(LoginForm, self).__init__(*args, **kwargs)
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
login_widget = forms.TextInput(
attrs={
"type": "email",
"placeholder": _("Email address"),
"autocomplete": "email",
}
)
login_field = forms.EmailField(label=_("Email"), widget=login_widget)
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME:
login_widget = forms.TextInput(
attrs={"placeholder": _("Username"), "autocomplete": "username"}
)
login_field = forms.CharField(
label=_("Username"),
widget=login_widget,
max_length=get_username_max_length(),
)
else:
assert (
app_settings.AUTHENTICATION_METHOD
== AuthenticationMethod.USERNAME_EMAIL
)
login_widget = forms.TextInput(
attrs={"placeholder": _("Username or email"), "autocomplete": "email"}
)
login_field = forms.CharField(
label=pgettext("field label", "Login"), widget=login_widget
)
self.fields["login"] = login_field
set_form_field_order(self, ["login", "password", "remember"])
if app_settings.SESSION_REMEMBER is not None:
del self.fields["remember"]
def user_credentials(self):
"""
Provides the credentials required to authenticate the user for
login.
"""
credentials = {}
login = self.cleaned_data["login"]
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
credentials["email"] = login
elif app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.USERNAME:
credentials["username"] = login
else:
if self._is_login_email(login):
credentials["email"] = login
credentials["username"] = login
credentials["password"] = self.cleaned_data["password"]
return credentials
def clean_login(self):
login = self.cleaned_data["login"]
return login.strip()
def _is_login_email(self, login):
try:
validators.validate_email(login)
ret = True
except exceptions.ValidationError:
ret = False
return ret
def clean(self):
super(LoginForm, self).clean()
if self._errors:
return
credentials = self.user_credentials()
user = get_adapter(self.request).authenticate(self.request, **credentials)
if user:
self.user = user
else:
auth_method = app_settings.AUTHENTICATION_METHOD
if auth_method == app_settings.AuthenticationMethod.USERNAME_EMAIL:
login = self.cleaned_data["login"]
if self._is_login_email(login):
auth_method = app_settings.AuthenticationMethod.EMAIL
else:
auth_method = app_settings.AuthenticationMethod.USERNAME
raise forms.ValidationError(
self.error_messages["%s_password_mismatch" % auth_method]
)
return self.cleaned_data
def login(self, request, redirect_url=None):
email = self.user_credentials().get("email")
ret = perform_login(
request,
self.user,
email_verification=app_settings.EMAIL_VERIFICATION,
redirect_url=redirect_url,
email=email,
)
remember = app_settings.SESSION_REMEMBER
if remember is None:
remember = self.cleaned_data["remember"]
if remember:
request.session.set_expiry(app_settings.SESSION_COOKIE_AGE)
else:
request.session.set_expiry(0)
return ret
class _DummyCustomSignupForm(forms.Form):
def signup(self, request, user):
"""
Invoked at signup time to complete the signup of the user.
"""
pass
def _base_signup_form_class():
"""
Currently, we inherit from the custom form, if any. This is all
not very elegant, though it serves a purpose:
- There are two signup forms: one for local accounts, and one for
social accounts
- Both share a common base (BaseSignupForm)
- Given the above, how to put in a custom signup form? Which form
would your custom form derive from, the local or the social one?
"""
if not app_settings.SIGNUP_FORM_CLASS:
return _DummyCustomSignupForm
try:
fc_module, fc_classname = app_settings.SIGNUP_FORM_CLASS.rsplit(".", 1)
except ValueError:
raise exceptions.ImproperlyConfigured(
"%s does not point to a form class" % app_settings.SIGNUP_FORM_CLASS
)
try:
mod = import_module(fc_module)
except ImportError as e:
raise exceptions.ImproperlyConfigured(
"Error importing form class %s:" ' "%s"' % (fc_module, e)
)
try:
fc_class = getattr(mod, fc_classname)
except AttributeError:
raise exceptions.ImproperlyConfigured(
'Module "%s" does not define a' ' "%s" class' % (fc_module, fc_classname)
)
if not hasattr(fc_class, "signup"):
raise exceptions.ImproperlyConfigured(
"The custom signup form must offer"
" a `def signup(self, request, user)` method",
)
return fc_class
class BaseSignupForm(_base_signup_form_class()):
username = forms.CharField(
label=_("Username"),
min_length=app_settings.USERNAME_MIN_LENGTH,
widget=forms.TextInput(
attrs={"placeholder": _("Username"), "autocomplete": "username"}
),
)
email = forms.EmailField(
widget=forms.TextInput(
attrs={
"type": "email",
"placeholder": _("Email address"),
"autocomplete": "email",
}
)
)
def __init__(self, *args, **kwargs):
email_required = kwargs.pop("email_required", app_settings.EMAIL_REQUIRED)
self.username_required = kwargs.pop(
"username_required", app_settings.USERNAME_REQUIRED
)
self.account_already_exists = False
super(BaseSignupForm, self).__init__(*args, **kwargs)
username_field = self.fields["username"]
username_field.max_length = get_username_max_length()
username_field.validators.append(
validators.MaxLengthValidator(username_field.max_length)
)
username_field.widget.attrs["maxlength"] = str(username_field.max_length)
default_field_order = [
"email",
"email2", # ignored when not present
"username",
"password1",
"password2", # ignored when not present
]
if app_settings.SIGNUP_EMAIL_ENTER_TWICE:
self.fields["email2"] = forms.EmailField(
label=_("Email (again)"),
widget=forms.TextInput(
attrs={
"type": "email",
"placeholder": _("Email address confirmation"),
}
),
)
if email_required:
self.fields["email"].label = gettext("Email")
self.fields["email"].required = True
else:
self.fields["email"].label = gettext("Email (optional)")
self.fields["email"].required = False
self.fields["email"].widget.is_required = False
if self.username_required:
default_field_order = [
"username",
"email",
"email2", # ignored when not present
"password1",
"password2", # ignored when not present
]
if not self.username_required:
del self.fields["username"]
set_form_field_order(
self, getattr(self, "field_order", None) or default_field_order
)
def clean_username(self):
value = self.cleaned_data["username"]
value = get_adapter().clean_username(value)
# Note regarding preventing enumeration: if the username is already
# taken, but the email address is not, we would still leak information
# if we were to send an email to that email address stating that the
# username is already in use.
return value
def clean_email(self):
value = self.cleaned_data["email"]
value = get_adapter().clean_email(value)
if value and app_settings.UNIQUE_EMAIL:
value = self.validate_unique_email(value)
return value
def validate_unique_email(self, value):
adapter = get_adapter()
assessment = assess_unique_email(value)
if assessment is True:
# All good.
pass
elif assessment is False:
# Fail right away.
raise forms.ValidationError(adapter.error_messages["email_taken"])
else:
assert assessment is None
self.account_already_exists = True
return adapter.validate_unique_email(value)
def clean(self):
cleaned_data = super(BaseSignupForm, self).clean()
if app_settings.SIGNUP_EMAIL_ENTER_TWICE:
email = cleaned_data.get("email")
email2 = cleaned_data.get("email2")
if (email and email2) and email != email2:
self.add_error("email2", _("You must type the same email each time."))
return cleaned_data
def custom_signup(self, request, user):
self.signup(request, user)
def try_save(self, request):
"""Try and save te user. This can fail in case of a conflict on the
email address, in that case we will send an "account already exists"
email and return a standard "email verification sent" response.
"""
if self.account_already_exists:
# Don't create a new account, only send an email informing the user
# that (s)he already has one...
email = self.cleaned_data["email"]
adapter = get_adapter()
adapter.send_account_already_exists_mail(email)
user = None
resp = adapter.respond_email_verification_sent(request, None)
else:
user = self.save(request)
resp = None
return user, resp
class SignupForm(BaseSignupForm):
def __init__(self, *args, **kwargs):
super(SignupForm, self).__init__(*args, **kwargs)
self.fields["password1"] = PasswordField(
label=_("Password"),
autocomplete="new-password",
help_text=password_validation.password_validators_help_text_html(),
)
if app_settings.SIGNUP_PASSWORD_ENTER_TWICE:
self.fields["password2"] = PasswordField(
label=_("Password (again)"), autocomplete="new-password"
)
if hasattr(self, "field_order"):
set_form_field_order(self, self.field_order)
def clean(self):
super(SignupForm, self).clean()
# `password` cannot be of type `SetPasswordField`, as we don't
# have a `User` yet. So, let's populate a dummy user to be used
# for password validation.
User = get_user_model()
dummy_user = User()
user_username(dummy_user, self.cleaned_data.get("username"))
user_email(dummy_user, self.cleaned_data.get("email"))
password = self.cleaned_data.get("password1")
if password:
try:
get_adapter().clean_password(password, user=dummy_user)
except forms.ValidationError as e:
self.add_error("password1", e)
if (
app_settings.SIGNUP_PASSWORD_ENTER_TWICE
and "password1" in self.cleaned_data
and "password2" in self.cleaned_data
):
if self.cleaned_data["password1"] != self.cleaned_data["password2"]:
self.add_error(
"password2",
_("You must type the same password each time."),
)
return self.cleaned_data
def save(self, request):
if self.account_already_exists:
raise ValueError(self.cleaned_data.get("email"))
adapter = get_adapter()
user = adapter.new_user(request)
adapter.save_user(request, user, self)
self.custom_signup(request, user)
# TODO: Move into adapter `save_user` ?
setup_user_email(request, user, [])
return user
class UserForm(forms.Form):
def __init__(self, user=None, *args, **kwargs):
self.user = user
super(UserForm, self).__init__(*args, **kwargs)
class AddEmailForm(UserForm):
email = forms.EmailField(
label=_("Email"),
required=True,
widget=forms.TextInput(
attrs={"type": "email", "placeholder": _("Email address")}
),
)
def clean_email(self):
from allauth.account import signals
value = self.cleaned_data["email"]
adapter = get_adapter()
value = adapter.clean_email(value)
errors = {
"this_account": _(
"This email address is already associated with this account."
),
"max_email_addresses": _("You cannot add more than %d email addresses."),
}
users = filter_users_by_email(value)
on_this_account = [u for u in users if u.pk == self.user.pk]
on_diff_account = [u for u in users if u.pk != self.user.pk]
if on_this_account:
raise forms.ValidationError(errors["this_account"])
if (
on_diff_account
and app_settings.PREVENT_ENUMERATION != "strict"
and app_settings.UNIQUE_EMAIL
):
raise forms.ValidationError(adapter.error_messages["email_taken"])
if not EmailAddress.objects.can_add_email(self.user):
raise forms.ValidationError(
errors["max_email_addresses"] % app_settings.MAX_EMAIL_ADDRESSES
)
signals._add_email.send(
sender=self.user.__class__,
email=value,
user=self.user,
)
return value
def save(self, request):
if app_settings.CHANGE_EMAIL:
return EmailAddress.objects.add_new_email(
request, self.user, self.cleaned_data["email"]
)
return EmailAddress.objects.add_email(
request, self.user, self.cleaned_data["email"], confirm=True
)
class ChangePasswordForm(PasswordVerificationMixin, UserForm):
oldpassword = PasswordField(
label=_("Current Password"), autocomplete="current-password"
)
password1 = SetPasswordField(
label=_("New Password"),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = PasswordField(label=_("New Password (again)"))
def __init__(self, *args, **kwargs):
super(ChangePasswordForm, self).__init__(*args, **kwargs)
self.fields["password1"].user = self.user
def clean_oldpassword(self):
if not self.user.check_password(self.cleaned_data.get("oldpassword")):
raise forms.ValidationError(_("Please type your current password."))
return self.cleaned_data["oldpassword"]
def save(self):
get_adapter().set_password(self.user, self.cleaned_data["password1"])
class SetPasswordForm(PasswordVerificationMixin, UserForm):
password1 = SetPasswordField(
label=_("Password"),
help_text=password_validation.password_validators_help_text_html(),
)
password2 = PasswordField(label=_("Password (again)"))
def __init__(self, *args, **kwargs):
super(SetPasswordForm, self).__init__(*args, **kwargs)
self.fields["password1"].user = self.user
def save(self):
get_adapter().set_password(self.user, self.cleaned_data["password1"])
class ResetPasswordForm(forms.Form):
email = forms.EmailField(
label=_("Email"),
required=True,
widget=forms.TextInput(
attrs={
"type": "email",
"placeholder": _("Email address"),
"autocomplete": "email",
}
),
)
def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email, is_active=True, prefer_verified=True)
if not self.users and not app_settings.PREVENT_ENUMERATION:
raise forms.ValidationError(
_("The email address is not assigned to any user account")
)
return self.cleaned_data["email"]
def save(self, request, **kwargs):
email = self.cleaned_data["email"]
if not self.users:
self._send_unknown_account_mail(request, email)
else:
self._send_password_reset_mail(request, email, self.users, **kwargs)
return email
def _send_unknown_account_mail(self, request, email):
signup_url = build_absolute_uri(request, reverse("account_signup"))
context = {
"current_site": get_current_site(request),
"email": email,
"request": request,
"signup_url": signup_url,
}
get_adapter().send_mail("account/email/unknown_account", email, context)
def _send_password_reset_mail(self, request, email, users, **kwargs):
token_generator = kwargs.get("token_generator", default_token_generator)
for user in users:
temp_key = token_generator.make_token(user)
# save it to the password reset model
# password_reset = PasswordReset(user=user, temp_key=temp_key)
# password_reset.save()
# send the password reset email
uid = user_pk_to_url_str(user)
path = reverse(
"account_reset_password_from_key",
kwargs=dict(uidb36=uid, key=temp_key),
)
url = build_absolute_uri(request, path)
context = {
"current_site": get_current_site(request),
"user": user,
"password_reset_url": url,
"uid": uid,
"key": temp_key,
"request": request,
}
if app_settings.AUTHENTICATION_METHOD != AuthenticationMethod.EMAIL:
context["username"] = user_username(user)
get_adapter().send_mail("account/email/password_reset_key", email, context)
class ResetPasswordKeyForm(PasswordVerificationMixin, forms.Form):
password1 = SetPasswordField(label=_("New Password"))
password2 = PasswordField(label=_("New Password (again)"))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
self.temp_key = kwargs.pop("temp_key", None)
super(ResetPasswordKeyForm, self).__init__(*args, **kwargs)
self.fields["password1"].user = self.user
def save(self):
get_adapter().set_password(self.user, self.cleaned_data["password1"])
class UserTokenForm(forms.Form):
uidb36 = forms.CharField()
key = forms.CharField()
reset_user = None
token_generator = default_token_generator
error_messages = {
"token_invalid": _("The password reset token was invalid."),
}
def _get_user(self, uidb36):
User = get_user_model()
try:
pk = url_str_to_user_pk(uidb36)
return User.objects.get(pk=pk)
except (ValueError, User.DoesNotExist):
return None
def clean(self):
cleaned_data = super(UserTokenForm, self).clean()
uidb36 = cleaned_data.get("uidb36", None)
key = cleaned_data.get("key", None)
if not key:
raise forms.ValidationError(self.error_messages["token_invalid"])
self.reset_user = self._get_user(uidb36)
if self.reset_user is None or not self.token_generator.check_token(
self.reset_user, key
):
raise forms.ValidationError(self.error_messages["token_invalid"])
return cleaned_data
class ReauthenticateForm(forms.Form):
password = PasswordField(label=_("Password"), autocomplete="current-password")
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)
def clean_password(self):
password = self.cleaned_data.get("password")
if not self.user.check_password(password):
raise forms.ValidationError(
get_adapter().error_messages["incorrect_password"]
)
return password

View File

@@ -0,0 +1,46 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.db.models import Count
from allauth.account.models import EmailAddress
from allauth.account.utils import user_email
class Command(BaseCommand):
def handle(self, *args, **options):
for user in self.get_users_with_multiple_primary_email():
self.unprimary_extra_primary_emails(user)
def get_users_with_multiple_primary_email(self):
user_pks = []
for email_address_dict in (
EmailAddress.objects.filter(primary=True)
.values("user")
.annotate(Count("user"))
.filter(user__count__gt=1)
):
user_pks.append(email_address_dict["user"])
return get_user_model().objects.filter(pk__in=user_pks)
def unprimary_extra_primary_emails(self, user):
primary_email_addresses = EmailAddress.objects.filter(user=user, primary=True)
for primary_email_address in primary_email_addresses:
if primary_email_address.email == user_email(user):
break
else:
# Didn't find the main email addresses and break the for loop
print(
"WARNING: Multiple primary without a user.email match for"
"user pk %s; (tried: %s, using: %s)"
) % (
user.pk,
", ".join(
[email_address.email for email_address in primary_email_addresses]
),
primary_email_address,
)
primary_email_addresses.exclude(pk=primary_email_address.pk).update(
primary=False
)

View File

@@ -0,0 +1,127 @@
import functools
from datetime import timedelta
from django.db import models
from django.db.models import Q
from django.utils import timezone
from . import app_settings
class EmailAddressManager(models.Manager):
def can_add_email(self, user):
ret = True
if app_settings.CHANGE_EMAIL:
# We always allow adding an email in this case, regardless of
# `MAX_EMAIL_ADDRESSES`, as adding actually adds a temporary email
# that the user wants to change to.
return True
elif app_settings.MAX_EMAIL_ADDRESSES:
count = self.filter(user=user).count()
ret = count < app_settings.MAX_EMAIL_ADDRESSES
return ret
def get_new(self, user):
"""
Returns the email address the user is in the process of changing to, if any.
"""
assert app_settings.CHANGE_EMAIL
return (
self.model.objects.filter(user=user, verified=False).order_by("pk").last()
)
def add_new_email(self, request, user, email):
"""
Adds an email address the user wishes to change to, replacing his
current email address once confirmed.
"""
assert app_settings.CHANGE_EMAIL
instance = self.get_new(user)
if not instance:
instance = self.model.objects.create(user=user, email=email)
else:
# Apparently, the user was already in the process of changing his
# email. Reuse that temporary email address.
instance.email = email
instance.verified = False
instance.primary = False
instance.save()
instance.send_confirmation(request)
return instance
def add_email(self, request, user, email, confirm=False, signup=False):
email_address, created = self.get_or_create(
user=user, email__iexact=email, defaults={"email": email}
)
if created and confirm:
email_address.send_confirmation(request, signup=signup)
return email_address
def get_verified(self, user):
return self.filter(user=user, verified=True).order_by("-primary", "pk").first()
def get_primary(self, user):
try:
return self.get(user=user, primary=True)
except self.model.DoesNotExist:
return None
def get_users_for(self, email):
# this is a list rather than a generator because we probably want to
# do a len() on it right away
return [
address.user for address in self.filter(verified=True, email__iexact=email)
]
def fill_cache_for_user(self, user, addresses):
"""
In a multi-db setup, inserting records and re-reading them later
on may result in not being able to find newly inserted
records. Therefore, we maintain a cache for the user so that
we can avoid database access when we need to re-read..
"""
user._emailaddress_cache = addresses
def get_for_user(self, user, email):
cache_key = "_emailaddress_cache"
addresses = getattr(user, cache_key, None)
if addresses is None:
ret = self.get(user=user, email__iexact=email)
# To avoid additional lookups when e.g.
# EmailAddress.set_as_primary() starts touching self.user
ret.user = user
return ret
else:
for address in addresses:
if address.email.lower() == email.lower():
return address
raise self.model.DoesNotExist()
def is_verified(self, email):
return self.filter(email__iexact=email, verified=True).exists()
def lookup(self, emails):
q_list = [Q(email__iexact=e) for e in emails]
if not q_list:
return self.none()
q = functools.reduce(lambda a, b: a | b, q_list)
return self.filter(q)
class EmailConfirmationManager(models.Manager):
def all_expired(self):
return self.filter(self.expired_q())
def all_valid(self):
return self.exclude(self.expired_q())
def expired_q(self):
sent_threshold = timezone.now() - timedelta(
days=app_settings.EMAIL_CONFIRMATION_EXPIRE_DAYS
)
return Q(sent__lt=sent_threshold)
def delete_expired_confirmations(self):
self.all_expired().delete()

View File

@@ -0,0 +1,33 @@
from django.conf import settings
from allauth.core import context
from allauth.core.exceptions import ImmediateHttpResponse
class AccountMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
with context.request_context(request):
response = self.get_response(request)
self._remove_dangling_login(request, response)
return response
def process_exception(self, request, exception):
if isinstance(exception, ImmediateHttpResponse):
return exception.response
def _remove_dangling_login(self, request, response):
if request.path.startswith(settings.STATIC_URL) or request.path in [
"/favicon.ico",
"/robots.txt",
"/humans.txt",
]:
return
if response.status_code // 100 != 2:
return
if not getattr(request, "_account_login_accessed", False):
if "account_login" in request.session:
request.session.pop("account_login")

View File

@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
UNIQUE_EMAIL = getattr(settings, "ACCOUNT_UNIQUE_EMAIL", True)
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="EmailAddress",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"email",
models.EmailField(
unique=UNIQUE_EMAIL,
max_length=75,
verbose_name="email address",
),
),
(
"verified",
models.BooleanField(default=False, verbose_name="verified"),
),
(
"primary",
models.BooleanField(default=False, verbose_name="primary"),
),
(
"user",
models.ForeignKey(
verbose_name="user",
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
),
),
],
options={
"verbose_name": "email address",
"verbose_name_plural": "email addresses",
},
bases=(models.Model,),
),
migrations.CreateModel(
name="EmailConfirmation",
fields=[
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
(
"created",
models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="created",
),
),
("sent", models.DateTimeField(null=True, verbose_name="sent")),
(
"key",
models.CharField(unique=True, max_length=64, verbose_name="key"),
),
(
"email_address",
models.ForeignKey(
verbose_name="email address",
to="account.EmailAddress",
on_delete=models.CASCADE,
),
),
],
options={
"verbose_name": "email confirmation",
"verbose_name_plural": "email confirmations",
},
bases=(models.Model,),
),
]
if not UNIQUE_EMAIL:
operations += [
migrations.AlterUniqueTogether(
name="emailaddress",
unique_together=set([("user", "email")]),
),
]

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
UNIQUE_EMAIL = getattr(settings, "ACCOUNT_UNIQUE_EMAIL", True)
EMAIL_MAX_LENGTH = getattr(settings, "ACCOUNT_EMAIL_MAX_LENGTH", 254)
class Migration(migrations.Migration):
dependencies = [
("account", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="emailaddress",
name="email",
field=models.EmailField(
unique=UNIQUE_EMAIL,
max_length=EMAIL_MAX_LENGTH,
verbose_name="email address",
),
),
]
if not UNIQUE_EMAIL:
operations += [
migrations.AlterUniqueTogether(
name="emailaddress",
unique_together=set([("user", "email")]),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 4.2.2 on 2023-06-14 12:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("account", "0002_email_max_length"),
]
operations = (
[
migrations.AlterUniqueTogether(
name="emailaddress",
unique_together={("user", "email")},
),
migrations.AddConstraint(
model_name="emailaddress",
constraint=models.UniqueConstraint(
condition=models.Q(("verified", True)),
fields=["email"],
name="unique_verified_email",
),
),
]
if getattr(settings, "ACCOUNT_UNIQUE_EMAIL", True)
else []
)

View File

@@ -0,0 +1,21 @@
from django.conf import settings
from django.db import migrations, models
EMAIL_MAX_LENGTH = getattr(settings, "ACCOUNT_EMAIL_MAX_LENGTH", 254)
class Migration(migrations.Migration):
dependencies = [
("account", "0003_alter_emailaddress_create_unique_verified_email"),
]
operations = [
migrations.AlterField(
model_name="emailaddress",
name="email",
field=models.EmailField(
max_length=EMAIL_MAX_LENGTH, verbose_name="email address"
),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.4 on 2023-08-23 18:17
import django.db.models.functions.text
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("account", "0004_alter_emailaddress_drop_unique_email"),
]
operations = [
migrations.AddIndex(
model_name="emailaddress",
index=models.Index(
django.db.models.functions.text.Upper("email"),
name="account_emailaddress_upper",
),
),
]

View File

@@ -0,0 +1,275 @@
import datetime
from django.contrib.auth import get_user_model
from django.core import signing
from django.db import models
from django.db.models import Index, Q
from django.db.models.constraints import UniqueConstraint
from django.db.models.functions import Upper
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .. import app_settings as allauth_app_settings
from . import app_settings, signals
from .adapter import get_adapter
from .managers import EmailAddressManager, EmailConfirmationManager
class EmailAddress(models.Model):
user = models.ForeignKey(
allauth_app_settings.USER_MODEL,
verbose_name=_("user"),
on_delete=models.CASCADE,
)
email = models.EmailField(
max_length=app_settings.EMAIL_MAX_LENGTH,
verbose_name=_("email address"),
)
verified = models.BooleanField(verbose_name=_("verified"), default=False)
primary = models.BooleanField(verbose_name=_("primary"), default=False)
objects = EmailAddressManager()
class Meta:
verbose_name = _("email address")
verbose_name_plural = _("email addresses")
unique_together = [("user", "email")]
if app_settings.UNIQUE_EMAIL:
constraints = [
UniqueConstraint(
fields=["email"],
name="unique_verified_email",
condition=Q(verified=True),
)
]
indexes = [Index(Upper("email"), name="account_emailaddress_upper")]
def __str__(self):
return self.email
def can_set_verified(self):
if self.verified:
return True
conflict = False
if app_settings.UNIQUE_EMAIL:
conflict = (
EmailAddress.objects.exclude(pk=self.pk)
.filter(verified=True, email__iexact=self.email)
.exists()
)
return not conflict
def set_verified(self, commit=True):
if self.verified:
return True
if self.can_set_verified():
self.verified = True
if commit:
self.save(update_fields=["verified"])
return self.verified
def set_as_primary(self, conditional=False):
"""Marks the email address as primary. In case of `conditional`, it is
only marked as primary if there is no other primary email address set.
"""
from allauth.account.utils import user_email
old_primary = EmailAddress.objects.get_primary(self.user)
if old_primary:
if conditional:
return False
old_primary.primary = False
old_primary.save()
self.primary = True
self.save()
user_email(self.user, self.email, commit=True)
return True
def send_confirmation(self, request=None, signup=False):
if app_settings.EMAIL_CONFIRMATION_HMAC:
confirmation = EmailConfirmationHMAC(self)
else:
confirmation = EmailConfirmation.create(self)
confirmation.send(request, signup=signup)
return confirmation
def remove(self):
from allauth.account.utils import user_email
self.delete()
if user_email(self.user) == self.email:
alt = (
EmailAddress.objects.filter(user=self.user)
.order_by("-verified")
.first()
)
alt_email = ""
if alt:
alt_email = alt.email
user_email(self.user, alt_email, commit=True)
class EmailConfirmationMixin:
def confirm(self, request):
email_address = self.email_address
if not email_address.verified:
confirmed = get_adapter().confirm_email(request, email_address)
if confirmed:
signals.email_confirmed.send(
sender=self.__class__,
request=request,
email_address=email_address,
)
return email_address
def send(self, request=None, signup=False):
get_adapter().send_confirmation_mail(request, self, signup)
signals.email_confirmation_sent.send(
sender=self.__class__,
request=request,
confirmation=self,
signup=signup,
)
class EmailConfirmation(EmailConfirmationMixin, models.Model):
email_address = models.ForeignKey(
EmailAddress,
verbose_name=_("email address"),
on_delete=models.CASCADE,
)
created = models.DateTimeField(verbose_name=_("created"), default=timezone.now)
sent = models.DateTimeField(verbose_name=_("sent"), null=True)
key = models.CharField(verbose_name=_("key"), max_length=64, unique=True)
objects = EmailConfirmationManager()
class Meta:
verbose_name = _("email confirmation")
verbose_name_plural = _("email confirmations")
def __str__(self):
return "confirmation for %s" % self.email_address
@classmethod
def create(cls, email_address):
key = get_adapter().generate_emailconfirmation_key(email_address.email)
return cls._default_manager.create(email_address=email_address, key=key)
def key_expired(self):
expiration_date = self.sent + datetime.timedelta(
days=app_settings.EMAIL_CONFIRMATION_EXPIRE_DAYS
)
return expiration_date <= timezone.now()
key_expired.boolean = True
def confirm(self, request):
if not self.key_expired():
return super().confirm(request)
def send(self, request=None, signup=False):
super().send(request=request, signup=signup)
self.sent = timezone.now()
self.save()
class EmailConfirmationHMAC(EmailConfirmationMixin, object):
def __init__(self, email_address):
self.email_address = email_address
@property
def key(self):
return signing.dumps(obj=self.email_address.pk, salt=app_settings.SALT)
@classmethod
def from_key(cls, key):
try:
max_age = 60 * 60 * 24 * app_settings.EMAIL_CONFIRMATION_EXPIRE_DAYS
pk = signing.loads(key, max_age=max_age, salt=app_settings.SALT)
ret = EmailConfirmationHMAC(EmailAddress.objects.get(pk=pk, verified=False))
except (
signing.SignatureExpired,
signing.BadSignature,
EmailAddress.DoesNotExist,
):
ret = None
return ret
class Login:
"""
Represents a user that is in the process of logging in.
"""
def __init__(
self,
user,
email_verification,
redirect_url=None,
signal_kwargs=None,
signup=False,
email=None,
state=None,
):
self.user = user
self.email_verification = email_verification
self.redirect_url = redirect_url
self.signal_kwargs = signal_kwargs
self.signup = signup
self.email = email
self.state = {} if state is None else state
def serialize(self):
from allauth.account.utils import user_pk_to_url_str
# :-( Knowledge of the `socialaccount` is entering the `account` app.
signal_kwargs = self.signal_kwargs
if signal_kwargs is not None:
sociallogin = signal_kwargs.get("sociallogin")
if sociallogin is not None:
signal_kwargs = signal_kwargs.copy()
signal_kwargs["sociallogin"] = sociallogin.serialize()
data = {
"user_pk": user_pk_to_url_str(self.user),
"email_verification": self.email_verification,
"signup": self.signup,
"redirect_url": self.redirect_url,
"email": self.email,
"signal_kwargs": signal_kwargs,
"state": self.state,
}
return data
@classmethod
def deserialize(cls, data):
from allauth.account.utils import url_str_to_user_pk
from allauth.socialaccount.models import SocialLogin
user = (
get_user_model()
.objects.filter(pk=url_str_to_user_pk(data["user_pk"]))
.first()
)
if user is None:
raise ValueError()
try:
# :-( Knowledge of the `socialaccount` is entering the `account` app.
signal_kwargs = data["signal_kwargs"]
if signal_kwargs is not None:
sociallogin = signal_kwargs.get("sociallogin")
if sociallogin is not None:
signal_kwargs = signal_kwargs.copy()
signal_kwargs["sociallogin"] = SocialLogin.deserialize(sociallogin)
return Login(
user=user,
email_verification=data["email_verification"],
redirect_url=data["redirect_url"],
signup=data["signup"],
signal_kwargs=signal_kwargs,
state=data["state"],
)
except KeyError:
raise ValueError()

View File

@@ -0,0 +1,33 @@
from django.contrib.auth.signals import user_logged_out # noqa
from django.dispatch import Signal
# Provides the arguments "request", "user"
user_logged_in = Signal()
# Typically followed by `user_logged_in` (unless, email verification kicks in)
# Provides the arguments "request", "user"
user_signed_up = Signal()
# Provides the arguments "request", "user"
password_set = Signal()
# Provides the arguments "request", "user"
password_changed = Signal()
# Provides the arguments "request", "user"
password_reset = Signal()
# Provides the arguments "request", "email_address"
email_confirmed = Signal()
# Provides the arguments "request", "confirmation", "signup"
email_confirmation_sent = Signal()
# Provides the arguments "request", "user", "from_email_address",
# "to_email_address"
email_changed = Signal()
# Provides the arguments "request", "user", "email_address"
email_added = Signal()
# Provides the arguments "request", "user", "email_address"
email_removed = Signal()
# Internal/private signal.
_add_email = Signal()

View File

@@ -0,0 +1,80 @@
from allauth.account.adapter import get_adapter
from allauth.account.utils import resume_login, stash_login, unstash_login
from allauth.utils import import_callable
class LoginStage:
key = None
def __init__(self, controller, request, login):
if not self.key:
raise ValueError()
self.controller = controller
self.request = request
self.login = login
def handle(self):
return None, True
def exit(self):
self.controller.set_handled(self.key)
return resume_login(self.request, self.login)
class LoginStageController:
def __init__(self, request, login):
self.request = request
self.login = login
self.state = self.login.state.setdefault("stages", {})
@classmethod
def enter(cls, request, stage_key):
login = unstash_login(request, peek=True)
if not login:
return None
ctrl = LoginStageController(request, login)
if ctrl.state.get("current") != stage_key:
return None
stages = ctrl.get_stages()
for stage in stages:
if stage.key == stage_key:
return stage
return None
def set_current(self, stage_key):
self.state["current"] = stage_key
def is_handled(self, stage_key):
return self.state.get(stage_key, {}).get("handled", False)
def set_handled(self, stage_key, data=None):
stage_state = self.state.setdefault(stage_key, {})
stage_state["handled"] = True
stage_state["data"] = data
def get_stages(self):
stages = []
adapter = get_adapter(self.request)
paths = adapter.get_login_stages()
for path in paths:
cls = import_callable(path)
stage = cls(self, self.request, self.login)
stages.append(stage)
return stages
def handle(self):
stages = self.get_stages()
for stage in stages:
if self.is_handled(stage.key):
continue
self.set_current(stage.key)
response, cont = stage.handle()
if response:
if cont:
stash_login(self.request, self.login)
else:
unstash_login(self.request)
return response
else:
assert cont
unstash_login(self.request)

View File

@@ -0,0 +1,24 @@
from django import template
from allauth.account.utils import user_display
register = template.Library()
@register.simple_tag(name="user_display")
def user_display_tag(user):
"""
Example usage::
{% user_display user %}
or if you need to use in a {% blocktrans %}::
{% user_display user as user_display %}
{% blocktrans %}
{{ user_display }} has sent you a gift.
{% endblocktrans %}
"""
return user_display(user)

View File

@@ -0,0 +1,22 @@
from django.http import HttpResponseRedirect
from django.urls import reverse
from allauth.account.adapter import DefaultAccountAdapter
from allauth.core.exceptions import ImmediateHttpResponse
class PreLoginRedirectAccountAdapter(DefaultAccountAdapter):
def pre_login(self, *args, **kwargs):
raise ImmediateHttpResponse(HttpResponseRedirect("/foo"))
def test_adapter_pre_login(settings, user, user_password, client):
settings.ACCOUNT_ADAPTER = (
"allauth.account.tests.test_adapter.PreLoginRedirectAccountAdapter"
)
resp = client.post(
reverse("account_login"),
{"login": user.username, "password": user_password},
)
assert resp.status_code == 302
assert resp["location"] == "/foo"

View File

@@ -0,0 +1,83 @@
from __future__ import absolute_import
import json
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import mail
from django.test.utils import override_settings
from django.urls import reverse
from allauth.account import app_settings
from allauth.tests import TestCase
class AjaxTests(TestCase):
def _send_post_request(self, **kwargs):
return self.client.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": "john@example.org",
"email2": "john@example.org",
"password1": "johndoe",
"password2": "johndoe",
},
**kwargs,
)
def test_no_ajax_header(self):
resp = self._send_post_request()
self.assertEqual(302, resp.status_code)
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
def test_ajax_header_x_requested_with(self):
resp = self._send_post_request(HTTP_X_REQUESTED_WITH="XMLHttpRequest")
self.assertEqual(200, resp.status_code)
self.assertEqual(settings.LOGIN_REDIRECT_URL, resp.json()["location"])
def test_ajax_header_http_accept(self):
resp = self._send_post_request(HTTP_ACCEPT="application/json")
self.assertEqual(200, resp.status_code)
self.assertEqual(settings.LOGIN_REDIRECT_URL, resp.json()["location"])
def test_ajax_password_reset(self):
get_user_model().objects.create(
username="john", email="john@example.org", is_active=True
)
resp = self.client.post(
reverse("account_reset_password"),
data={"email": "john@example.org"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["john@example.org"])
self.assertEqual(resp["content-type"], "application/json")
def test_ajax_login_fail(self):
resp = self.client.post(
reverse("account_login"),
{},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 400)
json.loads(resp.content.decode("utf8"))
# TODO: Actually test something
@override_settings(
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL
)
def test_ajax_login_success(self):
user = get_user_model().objects.create(username="john", is_active=True)
user.set_password("doe")
user.save()
resp = self.client.post(
reverse("account_login"),
{"login": "john", "password": "doe"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp.status_code, 200)
data = json.loads(resp.content.decode("utf8"))
self.assertEqual(data["location"], "/accounts/profile/")

View File

@@ -0,0 +1,75 @@
from __future__ import absolute_import
from django.contrib.auth import get_user_model
from django.test.utils import override_settings
from allauth.account import app_settings
from allauth.account.auth_backends import AuthenticationBackend
from allauth.tests import TestCase
class AuthenticationBackendTests(TestCase):
def setUp(self):
user = get_user_model().objects.create(
is_active=True, email="john@example.com", username="john"
)
user.set_password(user.username)
user.save()
self.user = user
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME
) # noqa
def test_auth_by_username(self):
user = self.user
backend = AuthenticationBackend()
self.assertEqual(
backend.authenticate(
request=None, username=user.username, password=user.username
).pk,
user.pk,
)
self.assertEqual(
backend.authenticate(
request=None, username=user.email, password=user.username
),
None,
)
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL
) # noqa
def test_auth_by_email(self):
user = self.user
backend = AuthenticationBackend()
self.assertEqual(
backend.authenticate(
request=None, username=user.email, password=user.username
).pk,
user.pk,
)
self.assertEqual(
backend.authenticate(
request=None, username=user.username, password=user.username
),
None,
)
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME_EMAIL
) # noqa
def test_auth_by_username_or_email(self):
user = self.user
backend = AuthenticationBackend()
self.assertEqual(
backend.authenticate(
request=None, username=user.email, password=user.username
).pk,
user.pk,
)
self.assertEqual(
backend.authenticate(
request=None, username=user.username, password=user.username
).pk,
user.pk,
)

View File

@@ -0,0 +1,304 @@
from __future__ import absolute_import
import json
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test.utils import override_settings
from django.urls import reverse
import pytest
from pytest_django.asserts import assertTemplateUsed
from allauth.account.models import EmailAddress, EmailConfirmationHMAC
from allauth.account.utils import user_email
from allauth.tests import TestCase
class ChangeEmailTests(TestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create(username="john", email="john1@example.org")
self.user.set_password("doe")
self.user.save()
self.email_address = EmailAddress.objects.create(
user=self.user, email=self.user.email, verified=True, primary=True
)
self.email_address2 = EmailAddress.objects.create(
user=self.user,
email="john2@example.org",
verified=False,
primary=False,
)
self.client.login(username="john", password="doe")
def test_ajax_get(self):
resp = self.client.get(
reverse("account_email"), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
)
data = json.loads(resp.content.decode("utf8"))
assert data["data"] == [
{
"id": self.email_address.pk,
"email": "john1@example.org",
"primary": True,
"verified": True,
},
{
"id": self.email_address2.pk,
"email": "john2@example.org",
"primary": False,
"verified": False,
},
]
def test_ajax_add(self):
resp = self.client.post(
reverse("account_email"),
{"action_add": "", "email": "john3@example.org"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
data = json.loads(resp.content.decode("utf8"))
self.assertEqual(data["location"], reverse("account_email"))
def test_ajax_add_invalid(self):
resp = self.client.post(
reverse("account_email"),
{"action_add": "", "email": "john3#example.org"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
data = json.loads(resp.content.decode("utf8"))
assert "valid" in data["form"]["fields"]["email"]["errors"][0]
def test_remove_primary(self):
resp = self.client.post(
reverse("account_email"),
{"action_remove": "", "email": self.email_address.email},
)
EmailAddress.objects.get(pk=self.email_address.pk)
self.assertTemplateUsed(
resp, "account/messages/cannot_delete_primary_email.txt"
)
def test_ajax_remove_primary(self):
resp = self.client.post(
reverse("account_email"),
{"action_remove": "", "email": self.email_address.email},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertTemplateUsed(
resp, "account/messages/cannot_delete_primary_email.txt"
)
data = json.loads(resp.content.decode("utf8"))
self.assertEqual(data["location"], reverse("account_email"))
def test_remove_secondary(self):
resp = self.client.post(
reverse("account_email"),
{"action_remove": "", "email": self.email_address2.email},
)
self.assertRaises(
EmailAddress.DoesNotExist,
lambda: EmailAddress.objects.get(pk=self.email_address2.pk),
)
self.assertTemplateUsed(resp, "account/messages/email_deleted.txt")
def test_set_primary_unverified(self):
resp = self.client.post(
reverse("account_email"),
{"action_primary": "", "email": self.email_address2.email},
)
email_address = EmailAddress.objects.get(pk=self.email_address.pk)
email_address2 = EmailAddress.objects.get(pk=self.email_address2.pk)
self.assertFalse(email_address2.primary)
self.assertTrue(email_address.primary)
self.assertTemplateUsed(resp, "account/messages/unverified_primary_email.txt")
def test_set_primary(self):
email_address2 = EmailAddress.objects.get(pk=self.email_address2.pk)
email_address2.verified = True
email_address2.save()
resp = self.client.post(
reverse("account_email"),
{"action_primary": "", "email": self.email_address2.email},
)
email_address = EmailAddress.objects.get(pk=self.email_address.pk)
email_address2 = EmailAddress.objects.get(pk=self.email_address2.pk)
self.assertFalse(email_address.primary)
self.assertTrue(email_address2.primary)
self.assertTemplateUsed(resp, "account/messages/primary_email_set.txt")
def test_verify(self):
resp = self.client.post(
reverse("account_email"),
{"action_send": "", "email": self.email_address2.email},
)
self.assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt")
def test_verify_unknown_email(self):
assert EmailAddress.objects.filter(user=self.user).count() == 2
self.client.post(
reverse("account_email"),
{"action_send": "", "email": "email@unknown.org"},
)
# This unknown email address must not be implicitly added.
assert EmailAddress.objects.filter(user=self.user).count() == 2
@override_settings(ACCOUNT_MAX_EMAIL_ADDRESSES=2)
def test_add_with_two_limiter(self):
resp = self.client.post(
reverse("account_email"), {"action_add": "", "email": "john3@example.org"}
)
self.assertTemplateNotUsed(resp, "account/messages/email_confirmation_sent.txt")
@override_settings(ACCOUNT_MAX_EMAIL_ADDRESSES=None)
def test_add_with_none_limiter(self):
resp = self.client.post(
reverse("account_email"), {"action_add": "", "email": "john3@example.org"}
)
self.assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt")
@override_settings(ACCOUNT_MAX_EMAIL_ADDRESSES=0)
def test_add_with_zero_limiter(self):
resp = self.client.post(
reverse("account_email"), {"action_add": "", "email": "john3@example.org"}
)
self.assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt")
def test_set_email_as_primary_doesnt_override_existed_changes_on_the_user(self):
user = get_user_model().objects.create(
username="@raymond.penners", first_name="Before Update"
)
email = EmailAddress.objects.create(
user=user,
email="raymond.penners@example.com",
primary=True,
verified=True,
)
updated_first_name = "Updated"
get_user_model().objects.filter(id=user.id).update(
first_name=updated_first_name
)
email.set_as_primary()
user.refresh_from_db()
self.assertEqual(user.first_name, updated_first_name)
@override_settings(ACCOUNT_USER_MODEL_EMAIL_FIELD=None)
def test_set_email_as_primary_doesnt_override_existed_changes_on_the_user_for_user_model_without_email_field(
self,
):
self.test_set_email_as_primary_doesnt_override_existed_changes_on_the_user()
def test_delete_email_changes_user_email(user_factory, client, email_factory):
user = user_factory(email_verified=False)
client.force_login(user)
first_email = EmailAddress.objects.get(user=user)
first_email.primary = False
first_email.save()
# other_unverified_email
EmailAddress.objects.create(
user=user, email=email_factory(), verified=False, primary=False
)
other_verified_email = EmailAddress.objects.create(
user=user, email=email_factory(), verified=True, primary=False
)
assert user_email(user) == first_email.email
resp = client.post(
reverse("account_email"),
{"action_remove": "", "email": first_email.email},
)
assert resp.status_code == 302
user.refresh_from_db()
assert user_email(user) == other_verified_email.email
def test_delete_email_wipes_user_email(user_factory, client):
user = user_factory(email_verified=False)
client.force_login(user)
first_email = EmailAddress.objects.get(user=user)
first_email.primary = False
first_email.save()
assert user_email(user) == first_email.email
resp = client.post(
reverse("account_email"),
{"action_remove": "", "email": first_email.email},
)
assert resp.status_code == 302
user.refresh_from_db()
assert user_email(user) == ""
def test_change_email(user_factory, client, settings):
settings.ACCOUNT_CHANGE_EMAIL = True
settings.ACCOUNT_EMAIL_CONFIRMATION_HMAC = True
user = user_factory(email_verified=True)
client.force_login(user)
current_email = EmailAddress.objects.get(user=user)
resp = client.post(
reverse("account_email"),
{"action_add": "", "email": "change-to@this.org"},
)
assert resp.status_code == 302
new_email = EmailAddress.objects.get(email="change-to@this.org")
key = EmailConfirmationHMAC(new_email).key
with patch("allauth.account.signals.email_changed.send") as email_changed_mock:
resp = client.post(reverse("account_confirm_email", args=[key]))
assert resp.status_code == 302
assert not EmailAddress.objects.filter(pk=current_email.pk).exists()
assert EmailAddress.objects.filter(user=user).count() == 1
new_email.refresh_from_db()
assert new_email.verified
assert new_email.primary
assert email_changed_mock.called
def test_add(auth_client, user, settings):
resp = auth_client.post(
reverse("account_email"),
{"action_add": "", "email": "john3@example.org"},
)
EmailAddress.objects.get(
email="john3@example.org",
user=user,
verified=False,
primary=False,
)
assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt")
@pytest.mark.parametrize(
"prevent_enumeration",
[
False,
True,
"strict",
],
)
def test_add_not_allowed(
auth_client, user, settings, user_factory, prevent_enumeration
):
settings.ACCOUNT_PREVENT_ENUMERATION = prevent_enumeration
email = "inuse@byotheruser.com"
user_factory(email=email)
resp = auth_client.post(
reverse("account_email"),
{"action_add": "", "email": email},
)
if prevent_enumeration == "strict":
assert resp.status_code == 302
EmailAddress.objects.get(
email=email,
user=user,
verified=False,
primary=False,
)
assertTemplateUsed(resp, "account/messages/email_confirmation_sent.txt")
else:
assert resp.status_code == 200
assert resp.context["form"].errors == {
"email": ["A user is already registered with this email address."]
}

View File

@@ -0,0 +1,7 @@
from django.core.management import call_command
def test_unset_multipleprimaryemails(db):
# This command needs to be dropped, in favor of having a conditional
# constraint.
call_command("account_unsetmultipleprimaryemails")

View File

@@ -0,0 +1,365 @@
from __future__ import absolute_import
from datetime import timedelta
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import mail
from django.http import HttpResponseRedirect
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.timezone import now
from allauth.account import app_settings
from allauth.account.models import (
EmailAddress,
EmailConfirmation,
EmailConfirmationHMAC,
)
from allauth.account.signals import user_logged_in
from allauth.account.utils import user_pk_to_url_str
from allauth.tests import Mock, TestCase, patch
from .test_models import UUIDUser
@override_settings(
ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME,
ACCOUNT_SIGNUP_FORM_CLASS=None,
ACCOUNT_EMAIL_SUBJECT_PREFIX=None,
LOGIN_REDIRECT_URL="/accounts/profile/",
ACCOUNT_SIGNUP_REDIRECT_URL="/accounts/welcome/",
ACCOUNT_ADAPTER="allauth.account.adapter.DefaultAccountAdapter",
ACCOUNT_USERNAME_REQUIRED=True,
)
class ConfirmationViewTests(TestCase):
def _create_user(self, username="john", password="doe", **kwargs):
user = get_user_model().objects.create(
username=username, is_active=True, **kwargs
)
if password:
user.set_password(password)
else:
user.set_unusable_password()
user.save()
return user
@override_settings(
ACCOUNT_EMAIL_CONFIRMATION_HMAC=True,
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION=True,
)
def test_login_on_confirm(self):
user = self._create_user()
email = EmailAddress.objects.create(
user=user, email="a@b.com", verified=False, primary=True
)
key = EmailConfirmationHMAC(email).key
receiver_mock = Mock() # we've logged if signal was called
user_logged_in.connect(receiver_mock)
# fake post-signup account_user stash
session = self.client.session
session["account_user"] = user_pk_to_url_str(user)
session.save()
resp = self.client.post(reverse("account_confirm_email", args=[key]))
email = EmailAddress.objects.get(pk=email.pk)
self.assertTrue(email.verified)
receiver_mock.assert_called_once_with(
sender=get_user_model(),
request=resp.wsgi_request,
response=resp,
user=get_user_model().objects.get(username="john"),
signal=user_logged_in,
)
user_logged_in.disconnect(receiver_mock)
@override_settings(
ACCOUNT_EMAIL_CONFIRMATION_HMAC=True,
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION=True,
)
@patch("allauth.account.views.perform_login")
@patch("allauth.account.utils.get_user_model", return_value=UUIDUser)
def test_login_on_confirm_uuid_user(self, mocked_gum, mock_perform_login):
user = UUIDUser(is_active=True, email="john@example.com", username="john")
# fake post-signup account_user stash
session = self.client.session
session["account_user"] = user_pk_to_url_str(user)
session.save()
# fake email and email confirmation to avoid swappable model hell
email = Mock(verified=False, user=user)
key = "mockkey"
confirmation = Mock(autospec=EmailConfirmationHMAC, key=key)
confirmation.email_address = email
confirmation.from_key.return_value = confirmation
mock_perform_login.return_value = HttpResponseRedirect(redirect_to="/")
with patch("allauth.account.views.EmailConfirmationHMAC", confirmation):
self.client.post(reverse("account_confirm_email", args=[key]))
assert mock_perform_login.called
@override_settings(
ACCOUNT_EMAIL_CONFIRMATION_HMAC=False,
)
def test_email_verification_failed(self):
verified_user = get_user_model().objects.create(username="foobar")
unverified_user = get_user_model().objects.create(username="foobar2")
EmailAddress.objects.create(
user=verified_user,
email="foo@bar.org",
verified=True,
primary=True,
)
email_address = EmailAddress.objects.create(
user=unverified_user,
email="foo@bar.org",
verified=False,
primary=False,
)
confirmation = EmailConfirmation.objects.create(
email_address=email_address,
key="dummy",
sent=now(),
)
c = Client()
resp = c.post(reverse("account_confirm_email", args=[confirmation.key]))
self.assertTemplateUsed(resp, "account/messages/email_confirmation_failed.txt")
@override_settings(
ACCOUNT_EMAIL_CONFIRMATION_HMAC=False, ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN=10
)
def test_email_verification_mandatory(self):
c = Client()
# Signup
resp = c.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": "john@example.com",
"password1": "johndoe",
"password2": "johndoe",
},
follow=True,
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(mail.outbox[0].to, ["john@example.com"])
self.assertGreater(mail.outbox[0].body.find("https://"), 0)
self.assertEqual(len(mail.outbox), 1)
self.assertTemplateUsed(
resp,
"account/verification_sent.%s" % app_settings.TEMPLATE_EXTENSION,
)
# Attempt to login, unverified
for attempt in [1, 2]:
resp = c.post(
reverse("account_login"),
{"login": "johndoe", "password": "johndoe"},
follow=True,
)
# is_active is controlled by the admin to manually disable
# users. I don't want this flag to flip automatically whenever
# users verify their email addresses.
self.assertTrue(
get_user_model()
.objects.filter(username="johndoe", is_active=True)
.exists()
)
self.assertTemplateUsed(
resp,
"account/verification_sent." + app_settings.TEMPLATE_EXTENSION,
)
# Attempt 1: no mail is sent due to cool-down ,
# but there was already a mail in the outbox.
self.assertEqual(len(mail.outbox), attempt)
self.assertEqual(
EmailConfirmation.objects.filter(
email_address__email="john@example.com"
).count(),
attempt,
)
# Wait for cooldown
EmailConfirmation.objects.update(sent=now() - timedelta(days=1))
# Verify, and re-attempt to login.
confirmation = EmailConfirmation.objects.filter(
email_address__user__username="johndoe"
)[:1].get()
resp = c.get(reverse("account_confirm_email", args=[confirmation.key]))
self.assertTemplateUsed(
resp, "account/email_confirm.%s" % app_settings.TEMPLATE_EXTENSION
)
c.post(reverse("account_confirm_email", args=[confirmation.key]))
resp = c.post(
reverse("account_login"),
{"login": "johndoe", "password": "johndoe"},
)
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
@override_settings(
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL,
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE=False,
)
def test_optional_email_verification(self):
c = Client()
# Signup
c.get(reverse("account_signup"))
resp = c.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": "john@example.com",
"password1": "johndoe",
},
)
# Logged in
self.assertRedirects(
resp, settings.ACCOUNT_SIGNUP_REDIRECT_URL, fetch_redirect_response=False
)
self.assertEqual(mail.outbox[0].to, ["john@example.com"])
self.assertEqual(len(mail.outbox), 1)
# Logout & login again
c.logout()
# Wait for cooldown
EmailConfirmation.objects.update(sent=now() - timedelta(days=1))
# Signup
resp = c.post(
reverse("account_login"),
{"login": "johndoe", "password": "johndoe"},
)
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
self.assertEqual(mail.outbox[0].to, ["john@example.com"])
# There was an issue that we sent out email confirmation mails
# on each login in case of optional verification. Make sure
# this is not the case:
self.assertEqual(len(mail.outbox), 1)
@override_settings(ACCOUNT_EMAIL_CONFIRMATION_HMAC=True)
def test_email_confirmation_hmac_falls_back(self):
user = self._create_user()
email = EmailAddress.objects.create(
user=user, email="a@b.com", verified=False, primary=True
)
confirmation = EmailConfirmation.create(email)
confirmation.sent = now()
confirmation.save()
self.client.post(reverse("account_confirm_email", args=[confirmation.key]))
email = EmailAddress.objects.get(pk=email.pk)
self.assertTrue(email.verified)
@override_settings(ACCOUNT_EMAIL_CONFIRMATION_HMAC=True)
def test_email_confirmation_hmac(self):
user = self._create_user()
email = EmailAddress.objects.create(
user=user, email="a@b.com", verified=False, primary=True
)
confirmation = EmailConfirmationHMAC(email)
request = RequestFactory().get("/")
confirmation.send(request=request)
self.assertEqual(len(mail.outbox), 1)
self.client.post(reverse("account_confirm_email", args=[confirmation.key]))
email = EmailAddress.objects.get(pk=email.pk)
self.assertTrue(email.verified)
@override_settings(
ACCOUNT_EMAIL_CONFIRMATION_HMAC=True,
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS=0,
)
def test_email_confirmation_hmac_timeout(self):
user = self._create_user()
email = EmailAddress.objects.create(
user=user, email="a@b.com", verified=False, primary=True
)
confirmation = EmailConfirmationHMAC(email)
request = RequestFactory().get("/")
confirmation.send(request=request)
self.assertEqual(len(mail.outbox), 1)
self.client.post(reverse("account_confirm_email", args=[confirmation.key]))
email = EmailAddress.objects.get(pk=email.pk)
self.assertFalse(email.verified)
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL
)
def test_confirm_email_with_another_user_logged_in(self):
"""Test the email confirmation view. If User B clicks on an email
verification link while logged in as User A, ensure User A gets
logged out."""
user = get_user_model().objects.create_user(
username="john", email="john@example.org", password="doe"
)
self.client.force_login(user)
self.client.post(
reverse("account_email"), {"email": user.email, "action_send": ""}
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [user.email])
self.client.logout()
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
user2 = self._create_user(username="john2", email="john2@example.com")
EmailAddress.objects.create(
user=user2, email=user2.email, primary=True, verified=True
)
resp = self.client.post(
reverse("account_login"),
{
"login": user2.email,
"password": "doe",
},
)
self.assertEqual(user2, resp.context["user"])
url = body[body.find("/confirm-email/") :].split()[0]
resp = self.client.post(url)
self.assertTemplateUsed(resp, "account/messages/logged_out.txt")
self.assertTemplateUsed(resp, "account/messages/email_confirmed.txt")
self.assertRedirects(resp, settings.LOGIN_URL, fetch_redirect_response=False)
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL
)
def test_confirm_email_with_same_user_logged_in(self):
"""Test the email confirmation view. If User A clicks on an email
verification link while logged in, ensure the user
stayed logged in."""
user = get_user_model().objects.create_user(
username="john", email="john@example.org", password="doe"
)
self.client.force_login(user)
self.client.post(
reverse("account_email"), {"email": user.email, "action_send": ""}
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, [user.email])
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
url = body[body.find("/confirm-email/") :].split()[0]
resp = self.client.post(url)
self.assertTemplateNotUsed(resp, "account/messages/logged_out.txt")
self.assertTemplateUsed(resp, "account/messages/email_confirmed.txt")
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
self.assertEqual(user, resp.wsgi_request.user)

View File

@@ -0,0 +1,314 @@
from __future__ import absolute_import
import json
import django
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import mail
from django.test.utils import override_settings
from django.urls import reverse
from allauth.account import app_settings
from allauth.account.models import EmailAddress
from allauth.tests import TestCase
@override_settings(
ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME,
ACCOUNT_SIGNUP_FORM_CLASS=None,
ACCOUNT_EMAIL_SUBJECT_PREFIX=None,
LOGIN_REDIRECT_URL="/accounts/profile/",
ACCOUNT_SIGNUP_REDIRECT_URL="/accounts/welcome/",
ACCOUNT_ADAPTER="allauth.account.adapter.DefaultAccountAdapter",
ACCOUNT_USERNAME_REQUIRED=True,
)
class LoginTests(TestCase):
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME_EMAIL
)
def test_username_containing_at(self):
user = get_user_model().objects.create(username="@raymond.penners")
user.set_password("psst")
user.save()
EmailAddress.objects.create(
user=user,
email="raymond.penners@example.com",
primary=True,
verified=True,
)
resp = self.client.post(
reverse("account_login"),
{"login": "@raymond.penners", "password": "psst"},
)
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
def _create_user(self, username="john", password="doe", **kwargs):
user = get_user_model().objects.create(
username=username, is_active=True, **kwargs
)
if password:
user.set_password(password)
else:
user.set_unusable_password()
user.save()
return user
def _create_user_and_login(self, usable_password=True):
password = "doe" if usable_password else False
user = self._create_user(password=password)
self.client.force_login(user)
return user
def test_redirect_when_authenticated(self):
self._create_user_and_login()
c = self.client
resp = c.get(reverse("account_login"))
self.assertRedirects(resp, "/accounts/profile/", fetch_redirect_response=False)
def test_ajax_password_change(self):
self._create_user_and_login()
resp = self.client.post(
reverse("account_change_password"),
data={
"oldpassword": "doe",
"password1": "AbCdEf!123",
"password2": "AbCdEf!123456",
},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(resp["content-type"], "application/json")
data = json.loads(resp.content.decode("utf8"))
assert "same password" in data["form"]["fields"]["password2"]["errors"][0]
@override_settings(
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL
)
def test_login_unverified_account_optional(self):
"""Tests login behavior when email verification is optional."""
user = get_user_model().objects.create(username="john")
user.set_password("doe")
user.save()
EmailAddress.objects.create(
user=user, email="user@example.com", primary=True, verified=False
)
resp = self.client.post(
reverse("account_login"), {"login": "john", "password": "doe"}
)
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
@override_settings(
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.OPTIONAL,
ACCOUNT_LOGIN_ATTEMPTS_LIMIT=3,
)
def test_login_failed_attempts_exceeded(self):
user = get_user_model().objects.create(username="john")
user.set_password("doe")
user.save()
EmailAddress.objects.create(
user=user, email="user@example.com", primary=True, verified=False
)
for i in range(5):
is_valid_attempt = i == 4
is_locked = i >= 3
resp = self.client.post(
reverse("account_login"),
{
"login": ["john", "John", "JOHN", "JOhn", "joHN"][i],
"password": ("doe" if is_valid_attempt else "wrong"),
},
)
if django.VERSION >= (4, 1):
self.assertFormError(
resp.context["form"],
None,
"Too many failed login attempts. Try again later."
if is_locked
else "The username and/or password you specified are not correct.",
)
else:
self.assertFormError(
resp,
"form",
None,
"Too many failed login attempts. Try again later."
if is_locked
else "The username and/or password you specified are not correct.",
)
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL,
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_LOGIN_ATTEMPTS_LIMIT=1,
)
def test_login_failed_attempts_exceeded_cleared_on_password_reset(self):
# Ensure that login attempts, once they hit the limit,
# can use the password reset mechanism to regain access.
user = get_user_model().objects.create(
username="john", email="john@example.org", is_active=True
)
user.set_password("doe")
user.save()
EmailAddress.objects.create(
user=user, email="john@example.org", primary=True, verified=True
)
resp = self.client.post(
reverse("account_login"), {"login": user.email, "password": "bad"}
)
if django.VERSION >= (4, 1):
self.assertFormError(
resp.context["form"],
None,
"The email address and/or password you specified are not correct.",
)
else:
self.assertFormError(
resp,
"form",
None,
"The email address and/or password you specified are not correct.",
)
resp = self.client.post(
reverse("account_login"), {"login": user.email, "password": "bad"}
)
if django.VERSION >= (4, 1):
self.assertFormError(
resp.context["form"],
None,
"Too many failed login attempts. Try again later.",
)
else:
self.assertFormError(
resp,
"form",
None,
"Too many failed login attempts. Try again later.",
)
self.client.post(reverse("account_reset_password"), data={"email": user.email})
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
# Extract URL for `password_reset_from_key` view and access it
url = body[body.find("/password/reset/") :].split()[0]
resp = self.client.get(url)
# Follow the redirect the actual password reset page with the key
# hidden.
url = resp.url
resp = self.client.get(url)
self.assertTemplateUsed(
resp,
"account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
)
self.assertFalse("token_fail" in resp.context_data)
new_password = "newpass123"
# Reset the password
resp = self.client.post(
url, {"password1": new_password, "password2": new_password}
)
self.assertRedirects(resp, reverse("account_reset_password_from_key_done"))
# Check the new password is in effect
user = get_user_model().objects.get(pk=user.pk)
self.assertTrue(user.check_password(new_password))
resp = self.client.post(
reverse("account_login"),
{"login": user.email, "password": new_password},
)
self.assertRedirects(
resp, settings.LOGIN_REDIRECT_URL, fetch_redirect_response=False
)
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL,
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_LOGIN_ATTEMPTS_LIMIT=1,
)
def test_login_using_unverified_email_address_is_prohibited(self):
user = get_user_model().objects.create(
username="john", email="john@example.org", is_active=True
)
user.set_password("doe")
user.save()
EmailAddress.objects.create(
user=user, email="john@example.org", primary=True, verified=True
)
EmailAddress.objects.create(
user=user, email="john@example.com", primary=True, verified=False
)
resp = self.client.post(
reverse("account_login"), {"login": "john@example.com", "password": "doe"}
)
self.assertRedirects(
resp,
reverse("account_email_verification_sent"),
fetch_redirect_response=False,
)
self.assertEqual(len(mail.outbox), 1)
assert mail.outbox[0].to == ["john@example.com"]
def test_login_unverified_account_mandatory(self):
"""Tests login behavior when email verification is mandatory."""
user = get_user_model().objects.create(username="john")
user.set_password("doe")
user.save()
EmailAddress.objects.create(
user=user, email="user@example.com", primary=True, verified=False
)
resp = self.client.post(
reverse("account_login"), {"login": "john", "password": "doe"}
)
self.assertRedirects(resp, reverse("account_email_verification_sent"))
def test_login_inactive_account(self):
"""
Tests login behavior with inactive accounts.
Inactive user accounts should be prevented from performing any actions,
regardless of their verified state.
"""
# Inactive and verified user account
user = get_user_model().objects.create(username="john", is_active=False)
user.set_password("doe")
user.save()
EmailAddress.objects.create(
user=user, email="john@example.com", primary=True, verified=True
)
resp = self.client.post(
reverse("account_login"), {"login": "john", "password": "doe"}
)
self.assertRedirects(resp, reverse("account_inactive"))
# Inactive and unverified user account
user = get_user_model().objects.create(username="doe", is_active=False)
user.set_password("john")
user.save()
EmailAddress.objects.create(
user=user, email="user@example.com", primary=True, verified=False
)
resp = self.client.post(
reverse("account_login"), {"login": "doe", "password": "john"}
)
self.assertRedirects(resp, reverse("account_inactive"))
@override_settings(ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS=False)
def test_account_authenticated_login_redirects_is_false(self):
self._create_user_and_login()
resp = self.client.get(reverse("account_login"))
self.assertEqual(resp.status_code, 200)

View File

@@ -0,0 +1,65 @@
from __future__ import absolute_import
from django.contrib.auth import get_user_model
from django.core import validators
from django.test.client import Client
from django.test.utils import override_settings
from django.urls import reverse
from allauth.account import app_settings
from allauth.account.signals import user_logged_out
from allauth.tests import Mock, TestCase
test_username_validators = [
validators.RegexValidator(regex=r"^[a-c]+$", message="not abc", flags=0)
]
@override_settings(
ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME,
ACCOUNT_SIGNUP_FORM_CLASS=None,
ACCOUNT_EMAIL_SUBJECT_PREFIX=None,
LOGIN_REDIRECT_URL="/accounts/profile/",
ACCOUNT_SIGNUP_REDIRECT_URL="/accounts/welcome/",
ACCOUNT_ADAPTER="allauth.account.adapter.DefaultAccountAdapter",
ACCOUNT_USERNAME_REQUIRED=True,
)
class LogoutTests(TestCase):
@override_settings(ACCOUNT_LOGOUT_ON_GET=True)
def test_logout_view_on_get(self):
c, resp = self._logout_view("get")
self.assertTemplateUsed(resp, "account/messages/logged_out.txt")
@override_settings(ACCOUNT_LOGOUT_ON_GET=False)
def test_logout_view_on_post(self):
c, resp = self._logout_view("get")
self.assertTemplateUsed(
resp, "account/logout.%s" % app_settings.TEMPLATE_EXTENSION
)
receiver_mock = Mock()
user_logged_out.connect(receiver_mock)
resp = c.post(reverse("account_logout"))
self.assertTemplateUsed(resp, "account/messages/logged_out.txt")
receiver_mock.assert_called_once_with(
sender=get_user_model(),
request=resp.wsgi_request,
user=get_user_model().objects.get(username="john"),
signal=user_logged_out,
)
user_logged_out.disconnect(receiver_mock)
def _logout_view(self, method):
c = Client()
user = get_user_model().objects.create(username="john", is_active=True)
user.set_password("doe")
user.save()
c = Client()
c.login(username="john", password="doe")
return c, getattr(c, method)(reverse("account_logout"))

View File

@@ -0,0 +1,25 @@
from django.conf import settings
from django.http import HttpResponse
import pytest
from allauth.account.middleware import AccountMiddleware
@pytest.mark.parametrize(
"path,status_code,login_removed",
[
("/", 200, True),
("/", 404, False),
(settings.STATIC_URL, 200, False),
("/favicon.ico", 200, False),
("/robots.txt", 200, False),
("/humans.txt", 200, False),
],
)
def test_remove_dangling_login(rf, path, status_code, login_removed):
request = rf.get(path)
request.session = {"account_login": True}
mw = AccountMiddleware(lambda request: HttpResponse(status=status_code))
mw(request)
assert ("account_login" in request.session) is (not login_removed)

View File

@@ -0,0 +1,27 @@
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from allauth.account.models import EmailAddress
class UUIDUser(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta(AbstractUser.Meta):
swappable = "AUTH_USER_MODEL"
def test_add_new_email(rf, user, settings):
settings.ACCOUNT_CHANGE_EMAIL = True
request = rf.get("/")
assert EmailAddress.objects.filter(user=user).count() == 1
new_email = EmailAddress.objects.add_new_email(request, user, "new@email.org")
assert not new_email.verified
assert not new_email.primary
assert EmailAddress.objects.filter(user=user).count() == 2
EmailAddress.objects.add_new_email(request, user, "new2@email.org")
assert EmailAddress.objects.filter(user=user).count() == 2
new_email.refresh_from_db()
assert new_email.email == "new2@email.org"

View File

@@ -0,0 +1,28 @@
from __future__ import absolute_import
from django.contrib.auth import get_user_model
from django.test.utils import override_settings
from django.urls import reverse
from allauth.tests import TestCase
@override_settings(
CACHES={
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
},
},
ACCOUNT_RATE_LIMITS={"reset_password_email": "1/m"},
)
class RateLimitTests(TestCase):
def test_case_insensitive_password_reset(self):
get_user_model().objects.create(email="a@b.com")
resp = self.client.post(
reverse("account_reset_password"), data={"email": "a@b.com"}
)
assert resp.status_code == 302
resp = self.client.post(
reverse("account_reset_password"), data={"email": "A@B.COM"}
)
assert resp.status_code == 429

View File

@@ -0,0 +1,302 @@
from __future__ import absolute_import
import json
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.core import mail
from django.test.utils import override_settings
from django.urls import reverse
from allauth.account import app_settings
from allauth.account.forms import ResetPasswordForm
from allauth.account.models import EmailAddress
from allauth.tests import TestCase
@override_settings(
ACCOUNT_PREVENT_ENUMERATION=False,
ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME,
ACCOUNT_SIGNUP_FORM_CLASS=None,
ACCOUNT_EMAIL_SUBJECT_PREFIX=None,
LOGIN_REDIRECT_URL="/accounts/profile/",
ACCOUNT_SIGNUP_REDIRECT_URL="/accounts/welcome/",
ACCOUNT_ADAPTER="allauth.account.adapter.DefaultAccountAdapter",
ACCOUNT_USERNAME_REQUIRED=True,
)
class ResetPasswordTests(TestCase):
def test_user_email_not_sent_inactive_user(self):
User = get_user_model()
User.objects.create_user(
"mike123", "mike@ixample.org", "test123", is_active=False
)
data = {"email": "mike@ixample.org"}
form = ResetPasswordForm(data)
self.assertFalse(form.is_valid())
def test_password_reset_get(self):
resp = self.client.get(reverse("account_reset_password"))
self.assertTemplateUsed(resp, "account/password_reset.html")
def test_password_set_redirect(self):
resp = self._password_set_or_change_redirect("account_set_password", True)
self.assertRedirects(
resp,
reverse("account_change_password"),
fetch_redirect_response=False,
)
def test_set_password_not_allowed(self):
user = self._create_user_and_login(True)
pwd = "!*123i1uwn12W23"
self.assertFalse(user.check_password(pwd))
resp = self.client.post(
reverse("account_set_password"),
data={"password1": pwd, "password2": pwd},
)
user.refresh_from_db()
self.assertFalse(user.check_password(pwd))
self.assertTrue(user.has_usable_password())
self.assertEqual(resp.status_code, 302)
def test_password_change_no_redirect(self):
resp = self._password_set_or_change_redirect("account_change_password", True)
self.assertEqual(resp.status_code, 200)
def test_password_set_no_redirect(self):
resp = self._password_set_or_change_redirect("account_set_password", False)
self.assertEqual(resp.status_code, 200)
def test_password_change_redirect(self):
resp = self._password_set_or_change_redirect("account_change_password", False)
self.assertRedirects(
resp,
reverse("account_set_password"),
fetch_redirect_response=False,
)
def test_password_forgotten_username_hint(self):
user = self._request_new_password()
body = mail.outbox[0].body
assert user.username in body
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL
)
def test_password_forgotten_no_username_hint(self):
user = self._request_new_password()
body = mail.outbox[0].body
assert user.username not in body
def _request_new_password(self):
user = get_user_model().objects.create(
username="john", email="john@example.org", is_active=True
)
user.set_password("doe")
user.save()
self.client.post(
reverse("account_reset_password"),
data={"email": "john@example.org"},
)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["john@example.org"])
return user
def test_password_reset_flow_with_empty_session(self):
"""
Test the password reset flow when the session is empty:
requesting a new password, receiving the reset link via email,
following the link, getting redirected to the
new link (without the token)
Copying the link and using it in a DIFFERENT client (Browser/Device).
"""
# Request new password
self._request_new_password()
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
# Extract URL for `password_reset_from_key` view
url = body[body.find("/password/reset/") :].split()[0]
resp = self.client.get(url)
reset_pass_url = resp.url
# Accessing the url via a different session
resp = self.client_class().get(reset_pass_url)
# We should receive the token_fail context_data
self.assertTemplateUsed(
resp,
"account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
)
self.assertTrue(resp.context_data["token_fail"])
def test_password_reset_flow(self):
"""
Tests the password reset flow: requesting a new password,
receiving the reset link via email and finally resetting the
password to a new value.
"""
# Request new password
user = self._request_new_password()
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
# Extract URL for `password_reset_from_key` view and access it
url = body[body.find("/password/reset/") :].split()[0]
resp = self.client.get(url)
# Follow the redirect the actual password reset page with the key
# hidden.
url = resp.url
resp = self.client.get(url)
self.assertTemplateUsed(
resp,
"account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
)
self.assertFalse("token_fail" in resp.context_data)
# Reset the password
resp = self.client.post(
url, {"password1": "newpass123", "password2": "newpass123"}
)
self.assertRedirects(resp, reverse("account_reset_password_from_key_done"))
# Check the new password is in effect
user = get_user_model().objects.get(pk=user.pk)
self.assertTrue(user.check_password("newpass123"))
# Trying to reset the password against the same URL (or any other
# invalid/obsolete URL) returns a bad token response
resp = self.client.post(
url, {"password1": "newpass123", "password2": "newpass123"}
)
self.assertTemplateUsed(
resp,
"account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
)
self.assertTrue(resp.context_data["token_fail"])
# Same should happen when accessing the page directly
response = self.client.get(url)
self.assertTemplateUsed(
response,
"account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
)
self.assertTrue(response.context_data["token_fail"])
# When in XHR views, it should respond with a 400 bad request
# code, and the response body should contain the JSON-encoded
# error from the adapter
response = self.client.post(
url,
{"password1": "newpass123", "password2": "newpass123"},
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 400)
data = json.loads(response.content.decode("utf8"))
assert "invalid" in data["form"]["errors"][0]
@override_settings(
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.EMAIL
)
def test_password_reset_flow_with_another_user_logged_in(self):
"""
Tests the password reset flow: if User B requested a password
reset earlier and now User A is logged in, User B now clicks on
the link, ensure User A is logged out before continuing.
"""
# Request new password
self._request_new_password()
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
user2 = self._create_user(username="john2", email="john2@example.com")
EmailAddress.objects.create(
user=user2, email=user2.email, primary=True, verified=True
)
resp = self.client.post(
reverse("account_login"),
{
"login": user2.email,
"password": "doe",
},
)
self.assertEqual(user2, resp.context["user"])
# Extract URL for `password_reset_from_key` view and access it
url = body[body.find("/password/reset/") :].split()[0]
resp = self.client.get(url)
# Follow the redirect the actual password reset page with the key
# hidden.
url = resp.url
resp = self.client.get(url)
self.assertTemplateUsed(
resp, "account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION
)
self.assertFalse("token_fail" in resp.context_data)
# Reset the password
resp = self.client.post(
url, {"password1": "newpass123", "password2": "newpass123"}, follow=True
)
self.assertRedirects(resp, reverse("account_reset_password_from_key_done"))
self.assertNotEqual(user2, resp.context["user"])
self.assertEqual(AnonymousUser(), resp.context["user"])
def test_password_reset_flow_with_email_changed(self):
"""
Test that the password reset token is invalidated if
the user email address was changed.
"""
user = self._request_new_password()
body = mail.outbox[0].body
self.assertGreater(body.find("https://"), 0)
EmailAddress.objects.create(user=user, email="other@email.org")
# Extract URL for `password_reset_from_key` view
url = body[body.find("/password/reset/") :].split()[0]
resp = self.client.get(url)
self.assertTemplateUsed(
resp,
"account/password_reset_from_key.%s" % app_settings.TEMPLATE_EXTENSION,
)
self.assertTrue("token_fail" in resp.context_data)
@override_settings(ACCOUNT_LOGIN_ON_PASSWORD_RESET=True)
def test_password_reset_ACCOUNT_LOGIN_ON_PASSWORD_RESET(self):
user = self._request_new_password()
body = mail.outbox[0].body
url = body[body.find("/password/reset/") :].split()[0]
resp = self.client.get(url)
# Follow the redirect the actual password reset page with the key
# hidden.
resp = self.client.post(
resp.url, {"password1": "newpass123", "password2": "newpass123"}
)
self.assertTrue(user.is_authenticated)
# EmailVerificationMethod.MANDATORY sends us to the confirm-email page
self.assertRedirects(resp, "/confirm-email/")
def _create_user(self, username="john", password="doe", **kwargs):
user = get_user_model().objects.create(
username=username, is_active=True, **kwargs
)
if password:
user.set_password(password)
else:
user.set_unusable_password()
user.save()
return user
def _create_user_and_login(self, usable_password=True):
password = "doe" if usable_password else False
user = self._create_user(password=password)
self.client.force_login(user)
return user
def _password_set_or_change_redirect(self, urlname, usable_password):
self._create_user_and_login(usable_password)
return self.client.get(reverse(urlname))

View File

@@ -0,0 +1,50 @@
from __future__ import absolute_import
from django.contrib.auth import get_user_model
from django.core import mail
from django.test.client import RequestFactory
from django.test.utils import override_settings
from allauth.account.forms import ResetPasswordForm
from allauth.tests import TestCase
@override_settings(ACCOUNT_PREVENT_ENUMERATION=False)
class TestCVE2019_19844(TestCase):
global_request = RequestFactory().get("/")
def test_user_email_unicode_collision(self):
User = get_user_model()
User.objects.create_user("mike123", "mike@example.org", "test123")
User.objects.create_user("mike456", "mıke@example.org", "test123")
data = {"email": "mıke@example.org"}
form = ResetPasswordForm(data)
self.assertTrue(form.is_valid())
form.save(self.global_request)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["mıke@example.org"])
def test_user_email_domain_unicode_collision(self):
User = get_user_model()
User.objects.create_user("mike123", "mike@ixample.org", "test123")
User.objects.create_user("mike456", "mike@ıxample.org", "test123")
data = {"email": "mike@ıxample.org"}
form = ResetPasswordForm(data)
self.assertTrue(form.is_valid())
form.save(self.global_request)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["mike@ıxample.org"])
def test_user_email_unicode_collision_nonexistent(self):
User = get_user_model()
User.objects.create_user("mike123", "mike@example.org", "test123")
data = {"email": "mıke@example.org"}
form = ResetPasswordForm(data)
self.assertFalse(form.is_valid())
def test_user_email_domain_unicode_collision_nonexistent(self):
User = get_user_model()
User.objects.create_user("mike123", "mike@ixample.org", "test123")
data = {"email": "mike@ıxample.org"}
form = ResetPasswordForm(data)
self.assertFalse(form.is_valid())

View File

@@ -0,0 +1,388 @@
from __future__ import absolute_import
import django
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.core import mail
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
import pytest
from pytest_django.asserts import assertTemplateUsed
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.forms import BaseSignupForm, SignupForm
from allauth.account.models import EmailAddress
from allauth.core import context
from allauth.tests import TestCase
from allauth.utils import get_username_max_length
class CustomSignupFormTests(TestCase):
@override_settings(
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE=True,
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE=True,
)
def test_custom_form_field_order(self):
expected_field_order = [
"email",
"email2",
"password1",
"password2",
"username",
"last_name",
"first_name",
]
class TestSignupForm(forms.Form):
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=30)
field_order = expected_field_order
class CustomSignupForm(SignupForm, TestSignupForm):
# ACCOUNT_SIGNUP_FORM_CLASS is only abided by when the
# BaseSignupForm definition is loaded the first time on Django
# startup. @override_settings() has therefore no effect.
pass
form = CustomSignupForm()
self.assertEqual(list(form.fields.keys()), expected_field_order)
def test_user_class_attribute(self):
from django.contrib.auth import get_user_model
from django.db.models.query_utils import DeferredAttribute
class CustomSignupForm(SignupForm):
# ACCOUNT_SIGNUP_FORM_CLASS is only abided by when the
# BaseSignupForm definition is loaded the first time on Django
# startup. @override_settings() has therefore no effect.
pass
User = get_user_model()
data = {
"username": "username",
"email": "user@example.com",
"password1": "very-secret",
"password2": "very-secret",
}
form = CustomSignupForm(data, email_required=True)
assert isinstance(User.username, DeferredAttribute)
form.is_valid()
assert isinstance(User.username, DeferredAttribute)
class BaseSignupFormTests(TestCase):
@override_settings(
ACCOUNT_USERNAME_REQUIRED=True, ACCOUNT_USERNAME_BLACKLIST=["username"]
)
def test_username_in_blacklist(self):
data = {
"username": "username",
"email": "user@example.com",
}
form = BaseSignupForm(data, email_required=True)
self.assertFalse(form.is_valid())
@override_settings(
ACCOUNT_USERNAME_REQUIRED=True, ACCOUNT_USERNAME_BLACKLIST=["username"]
)
def test_username_not_in_blacklist(self):
data = {
"username": "theusername",
"email": "user@example.com",
}
form = BaseSignupForm(data, email_required=True)
self.assertTrue(form.is_valid())
@override_settings(ACCOUNT_USERNAME_REQUIRED=True)
def test_username_maxlength(self):
data = {
"username": "username",
"email": "user@example.com",
}
form = BaseSignupForm(data, email_required=True)
max_length = get_username_max_length()
field = form.fields["username"]
self.assertEqual(field.max_length, max_length)
widget = field.widget
self.assertEqual(widget.attrs.get("maxlength"), str(max_length))
@override_settings(
ACCOUNT_USERNAME_REQUIRED=True, ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE=True
)
def test_signup_email_verification(self):
data = {
"username": "username",
"email": "user@example.com",
}
form = BaseSignupForm(data, email_required=True)
self.assertFalse(form.is_valid())
data = {
"username": "username",
"email": "user@example.com",
"email2": "user@example.com",
}
form = BaseSignupForm(data, email_required=True)
self.assertTrue(form.is_valid())
data["email2"] = "anotheruser@example.com"
form = BaseSignupForm(data, email_required=True)
self.assertFalse(form.is_valid())
@override_settings(
ACCOUNT_DEFAULT_HTTP_PROTOCOL="https",
ACCOUNT_EMAIL_VERIFICATION=app_settings.EmailVerificationMethod.MANDATORY,
ACCOUNT_AUTHENTICATION_METHOD=app_settings.AuthenticationMethod.USERNAME,
ACCOUNT_SIGNUP_FORM_CLASS=None,
ACCOUNT_EMAIL_SUBJECT_PREFIX=None,
LOGIN_REDIRECT_URL="/accounts/profile/",
ACCOUNT_SIGNUP_REDIRECT_URL="/accounts/welcome/",
ACCOUNT_ADAPTER="allauth.account.adapter.DefaultAccountAdapter",
ACCOUNT_USERNAME_REQUIRED=True,
)
class SignupTests(TestCase):
def test_signup_same_email_verified_externally(self):
user = self._test_signup_email_verified_externally(
"john@example.com", "john@example.com"
)
self.assertEqual(EmailAddress.objects.filter(user=user).count(), 1)
EmailAddress.objects.get(
verified=True, email="john@example.com", user=user, primary=True
)
def test_signup_other_email_verified_externally(self):
"""
John is invited on john@example.org, but signs up via john@example.com.
Email verification is by-passed, their home email address is
used as a secondary.
"""
user = self._test_signup_email_verified_externally(
"john@example.com", "john@example.org"
)
self.assertEqual(EmailAddress.objects.filter(user=user).count(), 2)
EmailAddress.objects.get(
verified=False, email="john@example.com", user=user, primary=False
)
EmailAddress.objects.get(
verified=True, email="john@example.org", user=user, primary=True
)
def _test_signup_email_verified_externally(self, signup_email, verified_email):
username = "johndoe"
request = RequestFactory().post(
reverse("account_signup"),
{
"username": username,
"email": signup_email,
"password1": "johndoe",
"password2": "johndoe",
},
)
# Fake stash_verified_email
SessionMiddleware(lambda request: None).process_request(request)
MessageMiddleware(lambda request: None).process_request(request)
request.user = AnonymousUser()
request.session["account_verified_email"] = verified_email
from allauth.account.views import signup
with context.request_context(request):
resp = signup(request)
self.assertEqual(resp.status_code, 302)
self.assertEqual(
resp["location"], get_adapter().get_signup_redirect_url(request)
)
self.assertEqual(len(mail.outbox), 0)
return get_user_model().objects.get(username=username)
@override_settings(
ACCOUNT_USERNAME_REQUIRED=True,
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE=True,
)
def test_signup_password_twice_form_error(self):
resp = self.client.post(
reverse("account_signup"),
data={
"username": "johndoe",
"email": "john@example.org",
"password1": "johndoe",
"password2": "janedoe",
},
)
if django.VERSION >= (4, 1):
self.assertFormError(
resp.context["form"],
"password2",
"You must type the same password each time.",
)
else:
self.assertFormError(
resp,
"form",
"password2",
"You must type the same password each time.",
)
@override_settings(
ACCOUNT_USERNAME_REQUIRED=True, ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE=True
)
def test_signup_email_twice(self):
request = RequestFactory().post(
reverse("account_signup"),
{
"username": "johndoe",
"email": "john@example.org",
"email2": "john@example.org",
"password1": "johndoe",
"password2": "johndoe",
},
)
SessionMiddleware(lambda request: None).process_request(request)
MessageMiddleware(lambda request: None).process_request(request)
request.user = AnonymousUser()
from allauth.account.views import signup
with context.request_context(request):
signup(request)
user = get_user_model().objects.get(username="johndoe")
self.assertEqual(user.email, "john@example.org")
@override_settings(
AUTH_PASSWORD_VALIDATORS=[
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {
"min_length": 9,
},
}
]
)
def test_django_password_validation(self):
resp = self.client.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": "john@example.com",
"password1": "johndoe",
"password2": "johndoe",
},
)
if django.VERSION >= (4, 1):
self.assertFormError(resp.context["form"], None, [])
self.assertFormError(
resp.context["form"],
"password1",
["This password is too short. It must contain at least 9 characters."],
)
else:
self.assertFormError(resp, "form", None, [])
self.assertFormError(
resp,
"form",
"password1",
["This password is too short. It must contain at least 9 characters."],
)
def test_prevent_enumeration_with_mandatory_verification(settings, user_factory):
settings.ACCOUNT_PREVENT_ENUMERATION = True
settings.ACCOUNT_AUTHENTICATION_METHOD = app_settings.AuthenticationMethod.EMAIL
settings.ACCOUNT_EMAIL_VERIFICATION = app_settings.EmailVerificationMethod.MANDATORY
user = user_factory(username="john", email="john@example.org", password="doe")
c = Client()
resp = c.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": user.email,
"password1": "johndoe",
"password2": "johndoe",
},
)
assert resp.status_code == 302
assert resp["location"] == reverse("account_email_verification_sent")
assertTemplateUsed(resp, "account/email/account_already_exists_message.txt")
assert EmailAddress.objects.filter(email="john@example.org").count() == 1
def test_prevent_enumeration_off(settings, user_factory):
settings.ACCOUNT_PREVENT_ENUMERATION = False
settings.ACCOUNT_AUTHENTICATION_METHOD = app_settings.AuthenticationMethod.EMAIL
settings.ACCOUNT_EMAIL_VERIFICATION = app_settings.EmailVerificationMethod.MANDATORY
user = user_factory(username="john", email="john@example.org", password="doe")
c = Client()
resp = c.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": user.email,
"password1": "johndoe",
"password2": "johndoe",
},
)
assert resp.status_code == 200
assert resp.context["form"].errors == {
"email": ["A user is already registered with this email address."]
}
def test_prevent_enumeration_strictly(settings, user_factory):
settings.ACCOUNT_PREVENT_ENUMERATION = "strict"
settings.ACCOUNT_AUTHENTICATION_METHOD = app_settings.AuthenticationMethod.EMAIL
settings.ACCOUNT_EMAIL_VERIFICATION = app_settings.EmailVerificationMethod.NONE
user = user_factory(username="john", email="john@example.org", password="doe")
c = Client()
resp = c.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": user.email,
"password1": "johndoe",
"password2": "johndoe",
},
)
assert resp.status_code == 302
assert resp["location"] == settings.LOGIN_REDIRECT_URL
assert EmailAddress.objects.filter(email="john@example.org").count() == 2
def test_prevent_enumeration_on(settings, user_factory):
settings.ACCOUNT_PREVENT_ENUMERATION = True
settings.ACCOUNT_AUTHENTICATION_METHOD = app_settings.AuthenticationMethod.EMAIL
settings.ACCOUNT_EMAIL_VERIFICATION = app_settings.EmailVerificationMethod.NONE
user = user_factory(username="john", email="john@example.org", password="doe")
c = Client()
resp = c.post(
reverse("account_signup"),
{
"username": "johndoe",
"email": user.email,
"password1": "johndoe",
"password2": "johndoe",
},
)
assert resp.status_code == 200
assert resp.context["form"].errors == {
"email": ["A user is already registered with this email address."]
}
@pytest.mark.django_db
def test_get_initial_with_valid_email():
"""Test that the email field is populated with a valid email."""
request = RequestFactory().get("/signup/?email=test@example.com")
from allauth.account.views import signup
SessionMiddleware(lambda request: None).process_request(request)
request.user = AnonymousUser()
with context.request_context(request):
view = signup(request)
assert view.context_data["view"].get_initial()["email"] == "test@example.com"

View File

@@ -0,0 +1,128 @@
from __future__ import absolute_import
import uuid
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.messages.api import get_messages
from django.contrib.messages.middleware import MessageMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from django.core import mail, validators
from django.core.exceptions import ValidationError
from django.template import Context, Template
from django.test.client import RequestFactory
from django.test.utils import override_settings
import allauth.app_settings
from allauth.account.adapter import get_adapter
from allauth.account.models import EmailAddress
from allauth.account.utils import (
filter_users_by_username,
url_str_to_user_pk,
user_pk_to_url_str,
user_username,
)
from allauth.core import context
from allauth.tests import TestCase, patch
from .test_models import UUIDUser
test_username_validators = [
validators.RegexValidator(regex=r"^[a-c]+$", message="not abc", flags=0)
]
class UtilsTests(TestCase):
def setUp(self):
self.user_id = uuid.uuid4().hex
def test_url_str_to_pk_identifies_UUID_as_stringlike(self):
with patch("allauth.account.utils.get_user_model") as mocked_gum:
mocked_gum.return_value = UUIDUser
self.assertEqual(url_str_to_user_pk(self.user_id), uuid.UUID(self.user_id))
def test_pk_to_url_string_identifies_UUID_as_stringlike(self):
with patch("allauth.account.utils.get_user_model") as mocked_gum:
mocked_gum.return_value = UUIDUser
user = UUIDUser(is_active=True, email="john@example.com", username="john")
self.assertEqual(user_pk_to_url_str(user), user.pk.hex)
@override_settings(ACCOUNT_PRESERVE_USERNAME_CASING=False)
def test_username_lower_cased(self):
user = get_user_model()()
user_username(user, "CamelCase")
self.assertEqual(user_username(user), "camelcase")
# TODO: Actually test something
filter_users_by_username("CamelCase", "FooBar")
@override_settings(ACCOUNT_PRESERVE_USERNAME_CASING=True)
def test_username_case_preserved(self):
user = get_user_model()()
user_username(user, "CamelCase")
self.assertEqual(user_username(user), "CamelCase")
# TODO: Actually test something
filter_users_by_username("camelcase", "foobar")
def test_user_display(self):
user = get_user_model()(username="john<br/>doe")
expected_name = "john&lt;br/&gt;doe"
templates = [
"{% load account %}{% user_display user %}",
"{% load account %}{% user_display user as x %}{{ x }}",
]
for template in templates:
t = Template(template)
content = t.render(Context({"user": user}))
self.assertEqual(content, expected_name)
def test_message_escaping(self):
request = RequestFactory().get("/")
SessionMiddleware(lambda request: None).process_request(request)
MessageMiddleware(lambda request: None).process_request(request)
user = get_user_model()()
user_username(user, "'<8")
context = {"user": user}
get_adapter().add_message(
request, messages.SUCCESS, "account/messages/logged_in.txt", context
)
msgs = get_messages(request)
actual_message = msgs._queued_messages[0].message
assert user.username in actual_message, actual_message
def test_email_escaping(self):
site_name = "testserver"
if allauth.app_settings.SITES_ENABLED:
from django.contrib.sites.models import Site
site = Site.objects.get_current()
site.name = site_name = '<enc&"test>'
site.save()
u = get_user_model().objects.create(username="test", email="user@example.com")
request = RequestFactory().get("/")
EmailAddress.objects.add_email(request, u, u.email, confirm=True)
self.assertTrue(mail.outbox[0].subject[1:].startswith(site_name))
@override_settings(
ACCOUNT_USERNAME_VALIDATORS="allauth.account.tests.test_utils.test_username_validators"
)
def test_username_validator(self):
get_adapter().clean_username("abc")
self.assertRaises(ValidationError, lambda: get_adapter().clean_username("def"))
@override_settings(ALLOWED_HOSTS=["allowed_host", "testserver"])
def test_is_safe_url_no_wildcard(self):
with context.request_context(RequestFactory().get("/")):
self.assertTrue(get_adapter().is_safe_url("http://allowed_host/"))
self.assertFalse(get_adapter().is_safe_url("http://other_host/"))
@override_settings(ALLOWED_HOSTS=["*"])
def test_is_safe_url_wildcard(self):
with context.request_context(RequestFactory().get("/")):
self.assertTrue(get_adapter().is_safe_url("http://foobar.com/"))
self.assertTrue(get_adapter().is_safe_url("http://other_host/"))
@override_settings(ALLOWED_HOSTS=["allowed_host", "testserver"])
def test_is_safe_url_relative_path(self):
with context.request_context(RequestFactory().get("/")):
self.assertTrue(get_adapter().is_safe_url("/foo/bar"))

View File

@@ -0,0 +1,47 @@
from django.urls import path, re_path
from . import views
urlpatterns = [
path("signup/", views.signup, name="account_signup"),
path("login/", views.login, name="account_login"),
path("logout/", views.logout, name="account_logout"),
path("reauthenticate/", views.reauthenticate, name="account_reauthenticate"),
path(
"password/change/",
views.password_change,
name="account_change_password",
),
path("password/set/", views.password_set, name="account_set_password"),
path("inactive/", views.account_inactive, name="account_inactive"),
# Email
path("email/", views.email, name="account_email"),
path(
"confirm-email/",
views.email_verification_sent,
name="account_email_verification_sent",
),
re_path(
r"^confirm-email/(?P<key>[-:\w]+)/$",
views.confirm_email,
name="account_confirm_email",
),
# password reset
path("password/reset/", views.password_reset, name="account_reset_password"),
path(
"password/reset/done/",
views.password_reset_done,
name="account_reset_password_done",
),
re_path(
r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$",
views.password_reset_from_key,
name="account_reset_password_from_key",
),
path(
"password/reset/key/done/",
views.password_reset_from_key_done,
name="account_reset_password_from_key_done",
),
]

View File

@@ -0,0 +1,587 @@
import time
import unicodedata
from collections import OrderedDict
from typing import Optional
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db.models import Q
from django.utils.encoding import force_str
from django.utils.http import base36_to_int, int_to_base36, urlencode
from allauth.account import app_settings, signals
from allauth.account.adapter import get_adapter
from allauth.account.models import Login
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.utils import (
get_request_param,
get_user_model,
import_callable,
valid_email_or_none,
)
def _unicode_ci_compare(s1, s2):
"""
Perform case-insensitive comparison of two identifiers, using the
recommended algorithm from Unicode Technical Report 36, section
2.11.2(B)(2).
"""
norm_s1 = unicodedata.normalize("NFKC", s1).casefold()
norm_s2 = unicodedata.normalize("NFKC", s2).casefold()
return norm_s1 == norm_s2
def get_next_redirect_url(request, redirect_field_name="next"):
"""
Returns the next URL to redirect to, if it was explicitly passed
via the request.
"""
redirect_to = get_request_param(request, redirect_field_name)
if not get_adapter().is_safe_url(redirect_to):
redirect_to = None
return redirect_to
def get_login_redirect_url(request, url=None, redirect_field_name="next", signup=False):
ret = url
if url and callable(url):
# In order to be able to pass url getters around that depend
# on e.g. the authenticated state.
ret = url()
if not ret:
ret = get_next_redirect_url(request, redirect_field_name=redirect_field_name)
if not ret:
if signup:
ret = get_adapter().get_signup_redirect_url(request)
else:
ret = get_adapter().get_login_redirect_url(request)
return ret
_user_display_callable = None
def logout_on_password_change(request, user):
# Since it is the default behavior of Django to invalidate all sessions on
# password change, this function actually has to preserve the session when
# logout isn't desired.
if not app_settings.LOGOUT_ON_PASSWORD_CHANGE:
update_session_auth_hash(request, user)
def default_user_display(user):
if app_settings.USER_MODEL_USERNAME_FIELD:
return getattr(user, app_settings.USER_MODEL_USERNAME_FIELD)
else:
return force_str(user)
def user_display(user):
global _user_display_callable
if not _user_display_callable:
f = getattr(settings, "ACCOUNT_USER_DISPLAY", default_user_display)
_user_display_callable = import_callable(f)
return _user_display_callable(user)
def user_field(user, field, *args, commit=False):
"""
Gets or sets (optional) user model fields. No-op if fields do not exist.
"""
if not field:
return
User = get_user_model()
try:
field_meta = User._meta.get_field(field)
max_length = field_meta.max_length
except FieldDoesNotExist:
if not hasattr(user, field):
return
max_length = None
if args:
# Setter
v = args[0]
if v:
v = v[0:max_length]
setattr(user, field, v)
if commit:
user.save(update_fields=[field])
else:
# Getter
return getattr(user, field)
def user_username(user, *args, commit=False):
if args and not app_settings.PRESERVE_USERNAME_CASING and args[0]:
args = [args[0].lower()]
return user_field(user, app_settings.USER_MODEL_USERNAME_FIELD, *args)
def user_email(user, *args, commit=False):
return user_field(user, app_settings.USER_MODEL_EMAIL_FIELD, *args, commit=commit)
def has_verified_email(user, email=None):
from .models import EmailAddress
emailaddress = None
if email:
ret = False
try:
emailaddress = EmailAddress.objects.get_for_user(user, email)
ret = emailaddress.verified
except EmailAddress.DoesNotExist:
pass
else:
ret = EmailAddress.objects.filter(user=user, verified=True).exists()
return ret
def perform_login(
request,
user,
email_verification,
redirect_url=None,
signal_kwargs=None,
signup=False,
email=None,
):
"""
Keyword arguments:
signup -- Indicates whether or not sending the
email is essential (during signup), or if it can be skipped (e.g. in
case email verification is optional and we are only logging in).
"""
login = Login(
user=user,
email_verification=email_verification,
redirect_url=redirect_url,
signal_kwargs=signal_kwargs,
signup=signup,
email=email,
)
return _perform_login(request, login)
def _perform_login(request, login):
# Local users are stopped due to form validation checking
# is_active, yet, adapter methods could toy with is_active in a
# `user_signed_up` signal. Furthermore, social users should be
# stopped anyway.
adapter = get_adapter()
hook_kwargs = _get_login_hook_kwargs(login)
response = adapter.pre_login(request, login.user, **hook_kwargs)
if response:
return response
return resume_login(request, login)
def _get_login_hook_kwargs(login):
"""
TODO: Just break backwards compatibility and pass only `login` to
`pre/post_login()`.
"""
return dict(
email_verification=login.email_verification,
redirect_url=login.redirect_url,
signal_kwargs=login.signal_kwargs,
signup=login.signup,
email=login.email,
)
def resume_login(request, login):
from allauth.account.stages import LoginStageController
adapter = get_adapter()
ctrl = LoginStageController(request, login)
try:
response = ctrl.handle()
if response:
return response
adapter.login(request, login.user)
hook_kwargs = _get_login_hook_kwargs(login)
response = adapter.post_login(request, login.user, **hook_kwargs)
if response:
return response
except ImmediateHttpResponse as e:
response = e.response
return response
def unstash_login(request, peek=False):
login = None
if peek:
data = request.session.get("account_login")
else:
data = request.session.pop("account_login", None)
if data is not None:
try:
login = Login.deserialize(data)
request._account_login_accessed = True
except ValueError:
pass
return login
def stash_login(request, login):
request.session["account_login"] = login.serialize()
request._account_login_accessed = True
def complete_signup(request, user, email_verification, success_url, signal_kwargs=None):
if signal_kwargs is None:
signal_kwargs = {}
signals.user_signed_up.send(
sender=user.__class__, request=request, user=user, **signal_kwargs
)
return perform_login(
request,
user,
email_verification=email_verification,
signup=True,
redirect_url=success_url,
signal_kwargs=signal_kwargs,
)
def cleanup_email_addresses(request, addresses):
"""
Takes a list of EmailAddress instances and cleans it up, making
sure only valid ones remain, without multiple primaries etc.
Order is important: e.g. if multiple primary email addresses
exist, the first one encountered will be kept as primary.
"""
from .models import EmailAddress
adapter = get_adapter()
# Let's group by `email`
e2a = OrderedDict() # maps email to EmailAddress
primary_addresses = []
verified_addresses = []
primary_verified_addresses = []
for address in addresses:
# Pick up only valid ones...
email = valid_email_or_none(address.email)
if not email:
continue
# ... and non-conflicting ones...
if (
app_settings.UNIQUE_EMAIL
and app_settings.PREVENT_ENUMERATION != "strict"
and EmailAddress.objects.lookup([email])
):
# Email address already exists.
continue
if (
app_settings.UNIQUE_EMAIL
and app_settings.PREVENT_ENUMERATION == "strict"
and address.verified
and EmailAddress.objects.is_verified(email)
):
# Email address already exists, and is verified as well.
continue
a = e2a.get(email.lower())
if a:
a.primary = a.primary or address.primary
a.verified = a.verified or address.verified
else:
a = address
a.verified = a.verified or adapter.is_email_verified(request, a.email)
e2a[email.lower()] = a
if a.primary:
primary_addresses.append(a)
if a.verified:
primary_verified_addresses.append(a)
if a.verified:
verified_addresses.append(a)
# Now that we got things sorted out, let's assign a primary
if primary_verified_addresses:
primary_address = primary_verified_addresses[0]
elif verified_addresses:
# Pick any verified as primary
primary_address = verified_addresses[0]
elif primary_addresses:
# Okay, let's pick primary then, even if unverified
primary_address = primary_addresses[0]
elif e2a:
# Pick the first
primary_address = e2a.keys()[0]
else:
# Empty
primary_address = None
# There can only be one primary
for a in e2a.values():
a.primary = primary_address.email.lower() == a.email.lower()
return list(e2a.values()), primary_address
def setup_user_email(request, user, addresses):
"""
Creates proper EmailAddress for the user that was just signed
up. Only sets up, doesn't do any other handling such as sending
out email confirmation mails etc.
"""
from .models import EmailAddress
assert not EmailAddress.objects.filter(user=user).exists()
priority_addresses = []
# Is there a stashed email?
adapter = get_adapter()
stashed_email = adapter.unstash_verified_email(request)
if stashed_email:
priority_addresses.append(
EmailAddress(user=user, email=stashed_email, primary=True, verified=True)
)
email = user_email(user)
if email:
priority_addresses.append(
EmailAddress(user=user, email=email, primary=True, verified=False)
)
addresses, primary = cleanup_email_addresses(
request, priority_addresses + addresses
)
for a in addresses:
a.user = user
a.save()
EmailAddress.objects.fill_cache_for_user(user, addresses)
if primary and email and email.lower() != primary.email.lower():
user_email(user, primary.email)
user.save()
return primary
def send_email_confirmation(request, user, signup=False, email=None):
"""
Email verification mails are sent:
a) Explicitly: when a user signs up
b) Implicitly: when a user attempts to log in using an unverified
email while EMAIL_VERIFICATION is mandatory.
Especially in case of b), we want to limit the number of mails
sent (consider a user retrying a few times), which is why there is
a cooldown period before sending a new mail. This cooldown period
can be configured in ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN setting.
"""
from .models import EmailAddress
adapter = get_adapter()
if not email:
email = user_email(user)
if email:
try:
email_address = EmailAddress.objects.get_for_user(user, email)
if not email_address.verified:
send_email = adapter.should_send_confirmation_mail(
request, email_address
)
if send_email:
email_address.send_confirmation(request, signup=signup)
else:
send_email = False
except EmailAddress.DoesNotExist:
send_email = True
email_address = EmailAddress.objects.add_email(
request, user, email, signup=signup, confirm=True
)
assert email_address
# At this point, if we were supposed to send an email we have sent it.
if send_email:
adapter.add_message(
request,
messages.INFO,
"account/messages/email_confirmation_sent.txt",
{"email": email, "login": not signup, "signup": signup},
)
if signup:
adapter.stash_user(request, user_pk_to_url_str(user))
def sync_user_email_addresses(user):
"""
Keep user.email in sync with user.emailaddress_set.
Under some circumstances the user.email may not have ended up as
an EmailAddress record, e.g. in the case of manually created admin
users.
"""
from .models import EmailAddress
email = user_email(user)
if (
email
and not EmailAddress.objects.filter(user=user, email__iexact=email).exists()
):
# get_or_create() to gracefully handle races
EmailAddress.objects.get_or_create(
user=user, email=email, defaults={"primary": False, "verified": False}
)
def filter_users_by_username(*username):
if app_settings.PRESERVE_USERNAME_CASING:
qlist = [
Q(**{app_settings.USER_MODEL_USERNAME_FIELD + "__iexact": u})
for u in username
]
q = qlist[0]
for q2 in qlist[1:]:
q = q | q2
ret = get_user_model()._default_manager.filter(q)
else:
ret = get_user_model()._default_manager.filter(
**{
app_settings.USER_MODEL_USERNAME_FIELD
+ "__in": [u.lower() for u in username]
}
)
return ret
def filter_users_by_email(email, is_active=None, prefer_verified=False):
"""Return list of users by email address
Typically one, at most just a few in length. First we look through
EmailAddress table, than customisable User model table. Add results
together avoiding SQL joins and deduplicate.
`prefer_verified`: When looking up users by email, there can be cases where
users with verified email addresses are preferable above users who did not
verify their email address. The password reset is such a use case -- if
there is a user with a verified email than that user should be returned, not
one of the other users.
"""
from .models import EmailAddress
User = get_user_model()
mails = EmailAddress.objects.filter(email__iexact=email).prefetch_related("user")
if is_active is not None:
mails = mails.filter(user__is_active=is_active)
mails = list(mails)
is_verified = False
if prefer_verified:
verified_mails = list(filter(lambda e: e.verified, mails))
if verified_mails:
mails = verified_mails
is_verified = True
users = []
for e in mails:
if _unicode_ci_compare(e.email, email):
users.append(e.user)
if app_settings.USER_MODEL_EMAIL_FIELD and not is_verified:
q_dict = {app_settings.USER_MODEL_EMAIL_FIELD + "__iexact": email}
user_qs = User.objects.filter(**q_dict)
if is_active is not None:
user_qs = user_qs.filter(is_active=is_active)
for user in user_qs.iterator():
user_email = getattr(user, app_settings.USER_MODEL_EMAIL_FIELD)
if _unicode_ci_compare(user_email, email):
users.append(user)
return list(set(users))
def passthrough_next_redirect_url(request, url, redirect_field_name):
assert url.find("?") < 0 # TODO: Handle this case properly
next_url = get_next_redirect_url(request, redirect_field_name)
if next_url:
url = url + "?" + urlencode({redirect_field_name: next_url})
return url
def user_pk_to_url_str(user):
"""
This should return a string.
"""
User = get_user_model()
pk_field_class = type(User._meta.pk)
if issubclass(pk_field_class, models.UUIDField):
if isinstance(user.pk, str):
return user.pk
return user.pk.hex
elif issubclass(pk_field_class, models.IntegerField):
return int_to_base36(int(user.pk))
return str(user.pk)
def url_str_to_user_pk(pk_str):
User = get_user_model()
remote_field = getattr(User._meta.pk, "remote_field", None)
if remote_field and getattr(remote_field, "to", None):
pk_field = User._meta.pk.remote_field.to._meta.pk
else:
pk_field = User._meta.pk
pk_field_class = type(pk_field)
if issubclass(pk_field_class, models.IntegerField):
pk = base36_to_int(pk_str)
# always call to_python() -- because there are fields like HashidField
# that derive from IntegerField.
pk = pk_field.to_python(pk)
else:
pk = pk_field.to_python(pk_str)
return pk
def record_authentication(request, user):
request.session["account_authenticated_at"] = time.time()
def did_recently_authenticate(request):
if request.user.is_anonymous:
return False
if not request.user.has_usable_password():
# TODO: This user only has social accounts attached. Now, ideally, you
# would want to reauthenticate over at the social account provider. For
# now, this is not implemented. Although definitely suboptimal, this
# method is currently used for reauthentication checks over at MFA, and,
# users that delegate the security of their account to an external
# provider like Google typically use MFA over there anyway.
return True
authenticated_at = request.session.get("account_authenticated_at")
if not authenticated_at:
return False
return time.time() - authenticated_at < app_settings.REAUTHENTICATION_TIMEOUT
def assess_unique_email(email) -> Optional[bool]:
"""
True -- email is unique
False -- email is already in use
None -- email is in use, but we should hide that using email verification.
"""
from .models import EmailAddress
if not EmailAddress.objects.lookup([email]).exists():
# All good.
return True
elif not app_settings.PREVENT_ENUMERATION:
# Fail right away.
return False
elif (
app_settings.EMAIL_VERIFICATION
== app_settings.EmailVerificationMethod.MANDATORY
):
# In case of mandatory verification and enumeration prevention,
# we can avoid creating a new account with the same (unverified)
# email address, because we are going to send an email anyway.
assert app_settings.PREVENT_ENUMERATION
return None
elif app_settings.PREVENT_ENUMERATION == "strict":
# We're going to be strict on enumeration prevention, and allow for
# this email address to pass even though it already exists. In this
# scenario, you can signup multiple times using the same email
# address resulting in multiple accounts with an unverified email.
return True
else:
assert app_settings.PREVENT_ENUMERATION is True
# Conflict. We're supposed to prevent enumeration, but we can't
# because that means letting the user in, while emails are required
# to be unique. In this case, uniqueness takes precedence over
# enumeration prevention.
return False

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
from django.conf import settings
SITES_ENABLED = "django.contrib.sites" in settings.INSTALLED_APPS
SOCIALACCOUNT_ENABLED = "allauth.socialaccount" in settings.INSTALLED_APPS
MFA_ENABLED = "allauth.mfa" in settings.INSTALLED_APPS
LOGIN_REDIRECT_URL = getattr(settings, "LOGIN_REDIRECT_URL", "/")
USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User")

View File

@@ -0,0 +1,85 @@
import uuid
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from allauth.account.models import EmailAddress
from allauth.account.utils import user_email, user_username
from allauth.core import context
from allauth.utils import get_user_model
@pytest.fixture
def user(user_factory):
return user_factory()
@pytest.fixture
def auth_client(client, user):
client.force_login(user)
return client
@pytest.fixture
def user_password():
return str(uuid.uuid4())
@pytest.fixture
def user_factory(email_factory, db, user_password):
def factory(
email=None,
username=None,
commit=True,
with_email=True,
email_verified=True,
password=None,
):
if not username:
username = uuid.uuid4().hex
if not email and with_email:
email = email_factory(username=username)
User = get_user_model()
user = User()
user.set_password(user_password if password is None else password)
user_username(user, username)
user_email(user, email or "")
if commit:
user.save()
if email:
EmailAddress.objects.create(
user=user, email=email, verified=email_verified, primary=True
)
return user
return factory
@pytest.fixture
def email_factory():
def factory(username=None):
if not username:
username = uuid.uuid4().hex
return f"{username}@{uuid.uuid4().hex}.org"
return factory
@pytest.fixture
def reauthentication_bypass():
@contextmanager
def f():
with patch("allauth.account.decorators.did_recently_authenticate") as m:
m.return_value = True
yield
return f
@pytest.fixture(autouse=True)
def clear_context_request():
context._request_var.set(None)

Some files were not shown because too many files have changed in this diff Show More