first commit

This commit is contained in:
pacnpal
2024-10-28 17:09:57 -04:00
commit 2e1b4d7af7
9993 changed files with 1182741 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
r"""
_ ___ __ __ .___________. __ __
/\| |/\ / \ | | | | | || | | |
\ ` ' / / ^ \ | | | | `---| |----`| |__| |
|_ _| / /_\ \ | | | | | | | __ |
/ , . \ / _____ \ | `--' | | | | | | |
\/|_|\//__/ \__\ \______/ |__| |__| |__|
"""
VERSION = (65, 1, 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-2024 Raymond Penners and contributors"

View File

@@ -0,0 +1,826 @@
import html
import json
import string
import warnings
from urllib.parse import urlparse
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 (
MinimumLengthValidator,
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 app_settings, signals
from allauth.core import context, ratelimit
from allauth.core.internal.adapter import BaseAdapter
from allauth.core.internal.httpkit import headed_redirect_response
from allauth.utils import generate_unique_username, import_attribute
class DefaultAccountAdapter(BaseAdapter):
"""The adapter class allows you to override various functionality of the
``allauth.account`` app. To do so, point ``settings.ACCOUNT_ADAPTER`` to
your own class that derives from ``DefaultAccountAdapter`` and override the
behavior by altering the implementation of the methods according to your own
needs.
"""
error_messages = {
"account_inactive": _("This account is currently inactive."),
"cannot_remove_primary_email": _(
"You cannot remove your primary email address."
),
"duplicate_email": _(
"This email address is already associated with this account."
),
"email_password_mismatch": _(
"The email address and/or password you specified are not correct."
),
"email_taken": _("A user is already registered with this email address."),
"enter_current_password": _("Please type your current password."),
"incorrect_code": _("Incorrect code."),
"incorrect_password": _("Incorrect password."),
"invalid_or_expired_key": _("Invalid or expired key."),
"invalid_password_reset": _("The password reset token was invalid."),
"max_email_addresses": _("You cannot add more than %d email addresses."),
"too_many_login_attempts": _(
"Too many failed login attempts. Try again later."
),
"unknown_email": _("The email address is not assigned to any user account"),
"unverified_primary_email": _("Your primary email address must be verified."),
"username_blacklisted": _(
"Username can not be used. Please use other username."
),
"username_password_mismatch": _(
"The username and/or password you specified are not correct."
),
"username_taken": AbstractUser._meta.get_field("username").error_messages[
"unique"
],
}
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 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 can_delete_email(self, email_address) -> bool:
"""
Returns whether or not the given email address can be deleted.
"""
from allauth.account.models import EmailAddress
has_other = (
EmailAddress.objects.filter(user_id=email_address.user_id)
.exclude(pk=email_address.pk)
.exists()
)
login_by_email = (
app_settings.AUTHENTICATION_METHOD
== app_settings.AuthenticationMethod.EMAIL
)
if email_address.primary:
if has_other:
# Don't allow, let the user mark one of the others as primary
# first.
return False
elif login_by_email:
# Last email & login is by email, prevent dangling account.
return False
return True
elif has_other:
# Account won't be dangling.
return True
elif login_by_email:
# This is the last email.
return False
else:
return True
def format_email_subject(self, subject) -> str:
"""
Formats the given email 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 = {}
html_ext = app_settings.TEMPLATE_EXTENSION
for ext in [html_ext, "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_ext in bodies:
msg.attach_alternative(bodies[html_ext], "text/html")
else:
msg = EmailMessage(
subject, bodies[html_ext], 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):
ctx = {
"email": email,
"current_site": get_current_site(globals()["context"].request),
}
ctx.update(context)
msg = self.render_mail(template_prefix, email, ctx)
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_verification_redirect_url(self, email_address):
"""
The URL to return to after email verification.
"""
get_url = getattr(self, "get_email_confirmation_redirect_url", None)
if get_url:
# Deprecated.
return get_url(self.request)
if self.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(self.request)
else:
return app_settings.EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL
def get_password_change_redirect_url(self, request):
"""
The URL to redirect to after a successful password change/set.
NOTE: Not called during the password reset flow.
"""
return reverse("account_change_password")
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"])
elif "password" in data:
user.set_password(data["password"])
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 self.validation_error("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():
raise self.validation_error("username_taken")
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:
MinimumLengthValidator(min_length).validate(password)
validate_password(password, user)
return password
def validate_unique_email(self, email):
return email
def add_message(
self,
request,
level,
message_template=None,
message_context=None,
extra_tags="",
message=None,
):
"""
Wrapper of `django.contrib.messages.add_message`, that reads
the message text from a template.
"""
if getattr(getattr(request, "allauth", None), "headless", None):
return
if "django.contrib.messages" in settings.INSTALLED_APPS:
if message:
messages.add_message(request, level, message, extra_tags=extra_tags)
return
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
):
if not user.is_active:
return self.respond_user_inactive(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):
# 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)
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.internal.flows import email_verification
return email_verification.verify_email(request, email_address)
def set_password(self, user, password) -> None:
"""
Sets the password for the user.
"""
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):
from django.utils.http import 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 send_password_reset_mail(self, user, email, context):
"""
Method intended to be overridden in case you need to customize the logic
used to determine whether a user is permitted to request a password reset.
For example, if you are enforcing something like "social only" authentication
in your app, you may want to intervene here by checking `user.has_usable_password`
"""
return self.send_mail("account/email/password_reset_key", email, context)
def get_reset_password_from_key_url(self, key):
"""
Method intended to be overridden in case the password reset email
needs to be adjusted.
"""
from allauth.account.internal import flows
return flows.password_reset.get_reset_password_from_key_url(self.request, key)
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.
"""
from allauth.account.internal import flows
return flows.email_verification.get_email_verification_url(
request, emailconfirmation
)
def should_send_confirmation_mail(self, request, email_address, signup) -> bool:
return True
def send_account_already_exists_mail(self, email):
from allauth.account.internal import flows
signup_url = flows.signup.get_signup_url(context.request)
password_reset_url = flows.password_reset.get_reset_password_url(
context.request
)
ctx = {
"request": context.request,
"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):
ctx = {
"user": emailconfirmation.email_address.user,
}
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
ctx.update({"code": emailconfirmation.key})
else:
ctx.update(
{
"key": emailconfirmation.key,
"activate_url": self.get_email_confirmation_url(
request, emailconfirmation
),
}
)
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 headed_redirect_response("account_inactive")
def respond_email_verification_sent(self, request, user):
return headed_redirect_response("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):
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):
cache_key = self._get_login_attempts_cache_key(request, **credentials)
if not ratelimit.consume(
request,
action="login_failed",
key=cache_key,
):
raise self.validation_error("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:
self._delete_login_attempts_cached_email(request, **credentials)
else:
self.authentication_failed(request, **credentials)
return user
def authentication_failed(self, request, **credentials):
pass
def reauthenticate(self, user, password):
from allauth.account.models import EmailAddress
from allauth.account.utils import user_username
credentials = {"[PASSWORD-REMOVED]}
username = user_username(user)
if username:
credentials["username"] = username
email = EmailAddress.objects.get_primary_email(user)
if email:
credentials["email"] = email
reauth_user = self.authenticate(context.request, **credentials)
return reauth_user is not None and reauth_user.pk == user.pk
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 get_http_user_agent(self, request):
return request.META.get("HTTP_USER_AGENT", "Unspecified")
def generate_emailconfirmation_key(self, email):
key = get_random_string(64).lower()
return key
def get_login_stages(self):
ret = []
ret.append("allauth.account.stages.LoginByCodeStage")
ret.append("allauth.account.stages.EmailVerificationStage")
if allauth_app_settings.MFA_ENABLED:
from allauth.mfa import app_settings as mfa_settings
ret.append("allauth.mfa.stages.AuthenticateStage")
if mfa_settings.PASSKEY_SIGNUP_ENABLED:
ret.append("allauth.mfa.webauthn.stages.PasskeySignupStage")
return ret
def get_reauthentication_methods(self, user):
"""The order of the methods returned matters. The first method is the
default when using the `@reauthentication_required` decorator.
"""
from allauth.account.internal.flows.reauthentication import (
get_reauthentication_flows,
)
flow_by_id = {f["id"]: f for f in get_reauthentication_flows(user)}
ret = []
if "reauthenticate" in flow_by_id:
entry = {
"id": "reauthenticate",
"description": _("Use your password"),
"url": reverse("account_reauthenticate"),
}
ret.append(entry)
if "mfa_reauthenticate" in flow_by_id:
types = flow_by_id["mfa_reauthenticate"]["types"]
if "recovery_codes" in types or "totp" in types:
entry = {
"id": "mfa_reauthenticate",
"description": _("Use authenticator app or code"),
"url": reverse("mfa_reauthenticate"),
}
ret.append(entry)
if "webauthn" in types:
entry = {
"id": "mfa_reauthenticate:webauthn",
"description": _("Use a security key"),
"url": reverse("mfa_reauthenticate_webauthn"),
}
ret.append(entry)
return ret
def send_notification_mail(self, template_prefix, user, context=None, email=None):
from allauth.account.models import EmailAddress
if not app_settings.EMAIL_NOTIFICATIONS:
return
if not email:
email = EmailAddress.objects.get_primary_email(user)
if not email:
return
ctx = {
"timestamp": timezone.now(),
"ip": self.get_client_ip(self.request),
"user_agent": self.get_http_user_agent(self.request),
}
if context:
ctx.update(context)
self.send_mail(template_prefix, email, ctx)
def generate_login_code(self) -> str:
"""
Generates a new login code.
"""
return self._generate_code()
def generate_email_verification_code(self) -> str:
"""
Generates a new email verification code.
"""
return self._generate_code()
def _generate_code(self):
forbidden_chars = "0OI18B2ZAEU"
allowed_chars = string.ascii_uppercase + string.digits
for ch in forbidden_chars:
allowed_chars = allowed_chars.replace(ch, "")
return get_random_string(length=6, allowed_chars=allowed_chars)
def is_login_by_code_required(self, login) -> bool:
"""
Returns whether or not login-by-code is required for the given
login.
"""
from allauth.account import authentication
method = None
records = authentication.get_authentication_records(self.request)
if records:
method = records[-1]["method"]
if method == "code":
return False
value = app_settings.LOGIN_BY_CODE_REQUIRED
if isinstance(value, bool):
return value
if not value:
return False
return method is None or method in value
def get_adapter(request=None):
return import_attribute(app_settings.ADAPTER)(request)

View File

@@ -0,0 +1,34 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
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") # type: ignore[attr-defined]
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,478 @@
import warnings
from enum import Enum
from typing import Set, Union
from django.core.exceptions import ImproperlyConfigured
class AppSettings:
class AuthenticationMethod(str, Enum):
USERNAME = "username"
EMAIL = "email"
USERNAME_EMAIL = "username_email"
class EmailVerificationMethod(str, Enum):
# 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):
from django.conf import settings
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"
)
if hasattr(settings, "ACCOUNT_LOGIN_ATTEMPTS_LIMIT") or hasattr(
settings, "ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT"
):
warnings.warn(
"settings.ACCOUNT_LOGIN_ATTEMPTS_LIMIT/TIMEOUT is deprecated, use: settings.ACCOUNT_RATE_LIMITS['login_failed']"
)
if hasattr(settings, "ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN"):
warnings.warn(
"settings.ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN is deprecated, use: settings.ACCOUNT_RATE_LIMITS['confirm_email']"
)
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_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 self.EmailVerificationMethod(ret)
@property
def EMAIL_VERIFICATION_BY_CODE_ENABLED(self):
return self._setting("EMAIL_VERIFICATION_BY_CODE_ENABLED", False)
@property
def EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS(self):
return self._setting("EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS", 3)
@property
def EMAIL_VERIFICATION_BY_CODE_TIMEOUT(self):
return self._setting("EMAIL_VERIFICATION_BY_CODE_TIMEOUT", 15 * 60)
@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 self.AuthenticationMethod(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):
rls = self._setting("RATE_LIMITS", {})
if rls is False:
return {}
attempts_amount = self._setting("LOGIN_ATTEMPTS_LIMIT", 5)
attempts_timeout = self._setting("LOGIN_ATTEMPTS_TIMEOUT", 60 * 5)
login_failed_rl = None
if attempts_amount and attempts_timeout:
login_failed_rl = f"10/m/ip,{attempts_amount}/{attempts_timeout}s/key"
if self.EMAIL_VERIFICATION_BY_CODE_ENABLED:
confirm_email_rl = "1/10s/key"
else:
cooldown = self._setting("EMAIL_CONFIRMATION_COOLDOWN", 3 * 60)
confirm_email_rl = None
if cooldown:
confirm_email_rl = f"1/{cooldown}s/key"
ret = {
# Change password view (for users already logged in)
"change_password": "5/m/user",
# Email management (e.g. add, remove, change primary)
"manage_email": "10/m/user",
# Request a password reset, global rate limit per IP
"reset_password": "20/m/ip,5/m/key",
# Reauthentication for users already logged in
"reauthenticate": "10/m/user",
# Password reset (the view the password reset email links to).
"reset_password_from_key": "20/m/ip",
# Signups.
"signup": "20/m/ip",
# Logins.
"login": "30/m/ip",
# Request a login code: key is the email.
"request_login_code": "20/m/ip,3/m/key",
# Logins.
"login_failed": login_failed_rl,
# Confirm email
"confirm_email": confirm_email_rl,
}
ret.update(rls)
return ret
@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 SIGNUP_FORM_HONEYPOT_FIELD(self):
"""
Honeypot field name. Empty string or ``None`` will disable honeypot behavior.
"""
return self._setting("SIGNUP_FORM_HONEYPOT_FIELD", 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 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 EMAIL_UNKNOWN_ACCOUNTS(self):
return self._setting("EMAIL_UNKNOWN_ACCOUNTS", True)
@property
def REAUTHENTICATION_TIMEOUT(self):
return self._setting("REAUTHENTICATION_TIMEOUT", 300)
@property
def EMAIL_NOTIFICATIONS(self):
return self._setting("EMAIL_NOTIFICATIONS", False)
@property
def REAUTHENTICATION_REQUIRED(self):
return self._setting("REAUTHENTICATION_REQUIRED", False)
@property
def LOGIN_BY_CODE_ENABLED(self):
return self._setting("LOGIN_BY_CODE_ENABLED", False)
@property
def LOGIN_BY_CODE_MAX_ATTEMPTS(self):
return self._setting("LOGIN_BY_CODE_MAX_ATTEMPTS", 3)
@property
def LOGIN_BY_CODE_TIMEOUT(self):
return self._setting("LOGIN_BY_CODE_TIMEOUT", 3 * 60)
@property
def LOGIN_TIMEOUT(self):
"""
The maximum allowed time (in seconds) for a login to go through the
various login stages. This limits, for example, the time span that the
2FA stage remains available.
"""
return self._setting("LOGIN_TIMEOUT", 15 * 60)
@property
def LOGIN_BY_CODE_REQUIRED(self) -> Union[bool, Set[str]]:
"""
When enabled (in case of ``True``), every user logging in is
required to input a login confirmation code sent by email.
Alternatively, you can specify a set of authentication methods
(``"password"``, ``"mfa"``, or ``"socialaccount"``) for which login
codes are required.
"""
value = self._setting("LOGIN_BY_CODE_REQUIRED", False)
if isinstance(value, bool):
return value
return set(value)
_app_settings = AppSettings("ACCOUNT_")
def __getattr__(name):
# See https://peps.python.org/pep-0562/
return getattr(_app_settings, name)

View File

@@ -0,0 +1,21 @@
from django.apps import AppConfig
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
from allauth import app_settings
class AccountConfig(AppConfig):
name = "allauth.account"
verbose_name = _("Accounts")
default_auto_field = app_settings.DEFAULT_AUTO_FIELD or "django.db.models.AutoField"
def ready(self):
from allauth.account import checks # noqa
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-REMOVED].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,7 @@
from allauth.account.internal.flows.login import (
AUTHENTICATION_METHODS_SESSION_KEY,
)
def get_authentication_records(request):
return request.session.get(AUTHENTICATION_METHODS_SESSION_KEY, [])

View File

@@ -0,0 +1,52 @@
from django.core.checks import Critical, Warning, register
@register()
def adapter_check(app_configs, **kwargs):
from allauth.account.adapter import get_adapter
ret = []
adapter = get_adapter()
if hasattr(adapter, "get_email_confirmation_redirect_url"):
ret.append(
Warning(
msg="adapter.get_email_confirmation_redirect_url(request) is deprecated, use adapter.get_email_verification_redirect_url(email_address)"
)
)
return ret
@register()
def settings_check(app_configs, **kwargs):
from allauth import app_settings as allauth_app_settings
from allauth.account import app_settings
ret = []
if allauth_app_settings.SOCIALACCOUNT_ONLY:
if app_settings.LOGIN_BY_CODE_ENABLED:
ret.append(
Critical(
msg="SOCIALACCOUNT_ONLY does not work with ACCOUNT_LOGIN_BY_CODE_ENABLED"
)
)
if allauth_app_settings.MFA_ENABLED:
ret.append(
Critical(msg="SOCIALACCOUNT_ONLY does not work with 'allauth.mfa'")
)
if app_settings.EMAIL_VERIFICATION != app_settings.EmailVerificationMethod.NONE:
ret.append(
Critical(
msg="SOCIALACCOUNT_ONLY requires ACCOUNT_EMAIL_VERIFICATION = 'none'"
)
)
if (
app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED
and app_settings.EMAIL_VERIFICATION
!= app_settings.EmailVerificationMethod.MANDATORY
):
ret.append(
Critical(
msg="ACCOUNT_EMAIL_VERFICATION_BY_CODE requires ACCOUNT_EMAIL_VERIFICATION = 'mandatory'"
)
)
return ret

View File

@@ -0,0 +1,102 @@
from functools import wraps
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.shortcuts import render, resolve_url
from allauth.account.internal.flows import reauthentication
from allauth.account.models import EmailAddress
from allauth.account.utils import (
get_next_redirect_url,
send_email_confirmation,
)
from allauth.core.exceptions import ReauthenticationRequired
from allauth.core.internal import httpkit
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,
allow_get=False,
enabled=None,
):
def decorator(view_func):
@wraps(view_func)
def _wrapper_view(request, *args, **kwargs):
pass_method = allow_get and request.method == "GET"
ena = (enabled is None) or (
enabled(request) if callable(enabled) else enabled
)
if ena and not pass_method:
if (
request.user.is_anonymous
or not reauthentication.did_recently_authenticate(request)
):
raise ReauthenticationRequired()
return view_func(request, *args, **kwargs)
return _wrapper_view
if function:
return decorator(function)
return decorator
def secure_admin_login(function=None):
def decorator(view_func):
@wraps(view_func)
def _wrapper_view(request, *args, **kwargs):
if request.user.is_authenticated:
if not request.user.is_staff or not request.user.is_active:
raise PermissionDenied()
return view_func(request, *args, **kwargs)
else:
next_url = get_next_redirect_url(request)
if not next_url:
next_url = request.get_full_path()
login_url = resolve_url(settings.LOGIN_URL)
login_url = httpkit.add_query_params(
login_url, {REDIRECT_FIELD_NAME: next_url}
)
return HttpResponseRedirect(login_url)
return _wrapper_view
if function:
return decorator(function)
return decorator

View File

@@ -0,0 +1,747 @@
from importlib import import_module
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import exceptions, validators
from django.urls import NoReverseMatch, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext, gettext_lazy as _, pgettext
from allauth.account.internal import flows
from allauth.account.internal.stagekit import LOGIN_SESSION_KEY
from allauth.account.stages import EmailVerificationStage
from allauth.core import context, ratelimit
from allauth.utils import get_username_max_length, set_form_field_order
from . import app_settings
from .adapter import DefaultAccountAdapter, get_adapter
from .app_settings import AuthenticationMethod
from .models import EmailAddress, Login
from .utils import (
filter_users_by_email,
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:
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"
kwargs.setdefault(
"help_text", password_validation.password_validators_help_text_html()
)
super().__init__(*args, **kwargs)
self.user = None
def clean(self, value):
value = super().clean(value)
value = get_adapter().clean_password(value, user=self.user)
return value
class LoginForm(forms.Form):
[PASSWORD-REMOVED](label=_("Password"), autocomplete="current-password")
remember = forms.BooleanField(label=_("Remember Me"), required=False)
user = None
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.EmailInput(
attrs={
"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"]
try:
reset_url = reverse("account_reset_password")
except NoReverseMatch:
pass
else:
forgot_txt = _("Forgot your password?")
self.fields["password"].help_text = mark_safe(
f'<a href="{reset_url}">{forgot_txt}</a>'
)
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()
adapter = get_adapter(self.request)
user = adapter.authenticate(self.request, **credentials)
if user:
login = Login(user=user, email=credentials.get("email"))
if flows.login.is_login_rate_limited(context.request, login):
raise adapter.validation_error("too_many_login_attempts")
self._login = login
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 adapter.validation_error("%s_password_mismatch" % auth_method.value)
return self.cleaned_data
def login(self, request, redirect_url=None):
credentials = self.user_credentials()
login = self._login
login.redirect_url = redirect_url
ret = flows.login.perform_password_login(request, credentials, login)
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()): # type: ignore[misc]
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"].lower()
value = get_adapter().clean_email(value)
if value and app_settings.UNIQUE_EMAIL:
value = self.validate_unique_email(value)
return value
def clean_email2(self):
value = self.cleaned_data["email2"].lower()
return value
def validate_unique_email(self, value):
adapter = get_adapter()
assessment = flows.manage_email.assess_unique_email(value)
if assessment is True:
# All good.
pass
elif assessment is False:
# Fail right away.
raise adapter.validation_error("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 the 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"]
resp = flows.signup.prevent_enumeration(request, email)
user = None
# Fake a login stage.
request.session[LOGIN_SESSION_KEY] = EmailVerificationStage.key
else:
user = self.save(request)
resp = None
return user, resp
def save(self, request):
email = self.cleaned_data.get("email")
if self.account_already_exists:
raise ValueError(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, [EmailAddress(email=email)] if email else [])
return user
class SignupForm(BaseSignupForm):
def __init__(self, *args, **kwargs):
self.by_passkey = kwargs.pop("by_passkey", False)
super(SignupForm, self).__init__(*args, **kwargs)
if not self.by_passkey:
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)
honeypot_field_name = app_settings.SIGNUP_FORM_HONEYPOT_FIELD
if honeypot_field_name:
self.fields[honeypot_field_name] = forms.CharField(
required=False,
label="",
widget=forms.TextInput(
attrs={
"style": "position: absolute; right: -99999px;",
"tabindex": "-1",
"autocomplete": "nope",
}
),
)
def try_save(self, request):
"""
override of parent class method that adds additional catching
of a potential bot filling out the honeypot field and returns a
'fake' email verification response if honeypot was filled out
"""
honeypot_field_name = app_settings.SIGNUP_FORM_HONEYPOT_FIELD
if honeypot_field_name:
if self.cleaned_data[honeypot_field_name]:
user = None
adapter = get_adapter()
# honeypot fields work best when you do not report to the bot
# that anything went wrong. So we return a fake email verification
# sent response but without creating a user
resp = adapter.respond_email_verification_sent(request, None)
return user, resp
return super().try_save(request)
def clean(self):
super().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
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"].lower()
adapter = get_adapter()
value = adapter.clean_email(value)
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 adapter.validation_error("duplicate_email")
if (
# Email is taken by a different account...
on_diff_account
# We care about not having duplicate emails
and app_settings.UNIQUE_EMAIL
# Enumeration prevention is turned off.
and (not app_settings.PREVENT_ENUMERATION)
):
raise adapter.validation_error("email_taken")
if not EmailAddress.objects.can_add_email(self.user):
raise adapter.validation_error(
"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.EMAIL_VERIFICATION_BY_CODE_ENABLED:
email_address = EmailAddress(
user=self.user, email=self.cleaned_data["email"]
)
email_address.send_confirmation(request)
return email_address
elif 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):
old[PASSWORD-REMOVED](
label=_("Current Password"), autocomplete="current-password"
)
password1 = SetPasswordField(label=_("New Password"))
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 get_adapter().validation_error("enter_current_password")
return self.cleaned_data["oldpassword"]
def save(self):
flows.password_change.change_password(self.user, self.cleaned_data["password1"])
class SetPasswordForm(PasswordVerificationMixin, UserForm):
password1 = SetPasswordField(label=_("Password"))
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):
flows.password_change.change_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"].lower()
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 get_adapter().validation_error("unknown_email")
return self.cleaned_data["email"]
def save(self, request, **kwargs) -> str:
email = self.cleaned_data["email"]
if not self.users:
flows.signup.send_unknown_account_mail(request, email)
return email
adapter: DefaultAccountAdapter = get_adapter()
token_generator = kwargs.get("token_generator", default_token_generator)
for user in self.users:
temp_key = token_generator.make_token(user)
# send the password reset email
uid = user_pk_to_url_str(user)
# We intentionally pass an opaque `key` on the interface here, and
# not implementation details such as a separate `uidb36` and
# `key. Ideally, this should have done on `urls` level as well.
key = f"{uid}-{temp_key}"
url = adapter.get_reset_password_from_key_url(key)
context = {
"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)
adapter.send_password_reset_mail(user, email, context)
return email
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):
flows.password_reset.reset_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
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)
adapter = get_adapter()
if not key:
raise adapter.validation_error("invalid_password_reset")
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 adapter.validation_error("invalid_password_reset")
return cleaned_data
class ReauthenticateForm(forms.Form):
[PASSWORD-REMOVED](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 get_adapter().reauthenticate(self.user, password):
raise get_adapter().validation_error("incorrect_password")
return password
class RequestLoginCodeForm(forms.Form):
email = forms.EmailField(
widget=forms.EmailInput(
attrs={
"placeholder": _("Email address"),
"autocomplete": "email",
}
)
)
def clean_email(self):
adapter = get_adapter()
email = self.cleaned_data["email"]
if not app_settings.PREVENT_ENUMERATION:
users = filter_users_by_email(email, is_active=True, prefer_verified=True)
if not users:
raise adapter.validation_error("unknown_email")
if not ratelimit.consume(
context.request, action="request_login_code", key=email.lower()
):
raise adapter.validation_error("too_many_login_attempts")
return email
class BaseConfirmCodeForm(forms.Form):
code = forms.CharField(
label=_("Code"),
widget=forms.TextInput(
attrs={"placeholder": _("Code"), "autocomplete": "one-time-code"},
),
)
def __init__(self, *args, **kwargs):
self.code = kwargs.pop("code")
super().__init__(*args, **kwargs)
def clean_code(self):
code = self.cleaned_data.get("code")
if not flows.login_by_code.compare_code(actual=code, expected=self.code):
raise get_adapter().validation_error("incorrect_code")
return code
class ConfirmLoginCodeForm(BaseConfirmCodeForm):
pass
class ConfirmEmailVerificationCodeForm(BaseConfirmCodeForm):
pass

View File

@@ -0,0 +1,4 @@
from allauth.account.internal import flows
__all__ = ["flows"]

View File

@@ -0,0 +1,37 @@
from functools import wraps
from django.contrib.auth import decorators
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.decorators.cache import never_cache
from allauth.account.stages import LoginStageController
from allauth.account.utils import get_login_redirect_url
def _dummy_login_not_required(view_func):
return view_func
login_not_required = getattr(
decorators, "login_not_required", _dummy_login_not_required
)
def login_stage_required(stage: str, redirect_urlname: str):
def decorator(view_func):
@never_cache
@login_not_required
@wraps(view_func)
def _wrapper_view(request, *args, **kwargs):
if request.user.is_authenticated:
return HttpResponseRedirect(get_login_redirect_url(request))
login_stage = LoginStageController.enter(request, stage)
if not login_stage:
return HttpResponseRedirect(reverse(redirect_urlname))
request._login_stage = login_stage
return view_func(request, *args, **kwargs)
return _wrapper_view
return decorator

View File

@@ -0,0 +1,26 @@
from allauth.account.internal.flows import (
email_verification,
email_verification_by_code,
login,
login_by_code,
logout,
manage_email,
password_change,
password_reset,
reauthentication,
signup,
)
__all__ = [
"password_reset",
"password_change",
"email_verification",
"email_verification_by_code",
"login",
"login_by_code",
"logout",
"signup",
"manage_email",
"reauthentication",
]

View File

@@ -0,0 +1,250 @@
from typing import Optional, Tuple
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from allauth.account import app_settings, signals
from allauth.account.adapter import get_adapter
from allauth.account.internal.flows.manage_email import emit_email_changed
from allauth.account.models import EmailAddress, Login
from allauth.core import ratelimit
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.core.internal.httpkit import get_frontend_url
from allauth.utils import build_absolute_uri
def verify_email_indirectly(request: HttpRequest, user, email: str) -> bool:
try:
email_address = EmailAddress.objects.get_for_user(user, email)
except EmailAddress.DoesNotExist:
return False
else:
if not email_address.verified:
return verify_email(request, email_address)
return True
def verify_email_and_resume(
request: HttpRequest, verification
) -> Tuple[Optional[EmailAddress], Optional[HttpResponse]]:
email_address = verification.confirm(request)
if not email_address:
return None, None
response = login_on_verification(request, verification)
return email_address, response
def verify_email(request: HttpRequest, email_address: EmailAddress) -> bool:
"""
Marks the email address as confirmed on the db
"""
added = not email_address.pk
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):
get_adapter(request).add_message(
request,
messages.ERROR,
"account/messages/email_confirmation_failed.txt",
{"email": email_address.email},
)
return False
email_address.set_as_primary(conditional=(not app_settings.CHANGE_EMAIL))
email_address.save()
if added:
signals.email_added.send(
sender=EmailAddress,
request=request,
user=request.user,
email_address=email_address,
)
signals.email_confirmed.send(
sender=EmailAddress,
request=request,
email_address=email_address,
)
if app_settings.CHANGE_EMAIL:
for instance in EmailAddress.objects.filter(
user_id=email_address.user_id
).exclude(pk=email_address.pk):
instance.remove()
emit_email_changed(request, from_email_address, email_address)
get_adapter(request).add_message(
request,
messages.SUCCESS,
"account/messages/email_confirmed.txt",
{"email": email_address.email},
)
return True
def get_email_verification_url(request: HttpRequest, emailconfirmation) -> str:
"""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 = get_frontend_url(request, "account_confirm_email", key=emailconfirmation.key)
if not url:
url = reverse("account_confirm_email", args=[emailconfirmation.key])
url = build_absolute_uri(request, url)
return url
def login_on_verification(request, verification) -> Optional[HttpResponse]:
"""Simply logging in the user may become a security issue. If you
do not take proper care (e.g. don't purge used email
confirmations), a malicious person that got hold of the link
will be able to login over and over again and the user is
unable to do anything about it. Even restoring their own mailbox
security will not help, as the links will still work. For
password reset this is different, this mechanism works only as
long as the attacker has access to the mailbox. If they no
longer has access they cannot issue a password request and
intercept it. Furthermore, all places where the links are
listed (log files, but even Google Analytics) all of a sudden
need to be secured. Purging the email confirmation once
confirmed changes the behavior -- users will not be able to
repeatedly confirm (in case they forgot that they already
clicked the mail).
All in all, we only login on verification when the user that is in the
process of signing up is present in the session to avoid all of the above.
This may not 100% work in case the user closes the browser (and the session
gets lost), but at least we're secure.
"""
from allauth.account.stages import (
EmailVerificationStage,
LoginStageController,
)
if not app_settings.LOGIN_ON_EMAIL_CONFIRMATION:
return None
if request.user.is_authenticated:
return None
stage = LoginStageController.enter(request, EmailVerificationStage.key)
if not stage or not stage.login.user:
return None
if stage.login.user.pk != verification.email_address.user_id:
return None
return stage.exit()
def consume_email_verification_rate_limit(
request: HttpRequest, email: str, dry_run: bool = False
) -> bool:
return ratelimit.consume(
request, action="confirm_email", key=email.lower(), dry_run=dry_run
)
def handle_verification_email_rate_limit(request, email: str) -> bool:
"""
For email verification by link, it is not an issue if the user runs into rate
limits. The reason is that the link is session independent. Therefore, if the
user hits rate limits, we can just silently skip sending additional
verification emails, as the previous emails that were already sent still
contain valid links. This is different from email verification by code. Here,
the session contains a specific code, meaning, silently skipping new
verification emails is not an option, and we must hard fail (429) instead. The
latter was missing, fixed.
"""
rl_ok = consume_email_verification_rate_limit(request, email)
if not rl_ok and app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
raise ImmediateHttpResponse(ratelimit.respond_429(request))
return rl_ok
def send_verification_email(request, user, signup=False, email=None) -> bool:
"""
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.
TODO: This code is doing way too much. Looking up EmailAddress, creating
if not present, etc. To be refactored.
"""
from allauth.account.utils import user_email
adapter = get_adapter()
sent = False
email_address = None
if not email:
email = user_email(user)
if not email:
email_address = (
EmailAddress.objects.filter(user=user).order_by("verified", "pk").first()
)
if email_address:
email = email_address.email
if email:
if email_address is None:
try:
email_address = EmailAddress.objects.get_for_user(user, email)
except EmailAddress.DoesNotExist:
pass
if email_address is not None:
if not email_address.verified:
send_email = handle_verification_email_rate_limit(
request, email_address.email
)
if send_email:
send_email = adapter.should_send_confirmation_mail(
request, email_address, signup
)
if send_email:
email_address.send_confirmation(request, signup=signup)
sent = True
else:
send_email = False
else:
send_email = True
email_address = EmailAddress.objects.add_email(
request, user, email, signup=signup, confirm=True
)
sent = 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},
)
return sent
def is_verification_rate_limited(request: HttpRequest, login: Login) -> bool:
"""
Returns whether or not the email verification is *hard* rate limited.
Hard, meaning, it would be blocking login (verification by code, not link).
"""
if (
(not login.email)
or (not app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED)
or login.email_verification != app_settings.EmailVerificationMethod.MANDATORY
):
return False
try:
email_address = EmailAddress.objects.get_for_user(login.user, login.email)
if not email_address.verified:
if not consume_email_verification_rate_limit(
request, login.email, dry_run=True
):
return True
except EmailAddress.DoesNotExist:
pass
return False

View File

@@ -0,0 +1,112 @@
import time
from typing import Any, Dict, Optional, Tuple
from django.contrib.auth import get_user_model
from django.http import HttpRequest
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.internal.flows.login_by_code import compare_code
from allauth.account.internal.stagekit import clear_login
from allauth.account.models import EmailAddress, EmailConfirmationMixin
from allauth.core import context
EMAIL_VERIFICATION_CODE_SESSION_KEY = "account_email_verification_code"
class EmailVerificationModel(EmailConfirmationMixin):
def __init__(self, email_address: EmailAddress, key: Optional[str] = None):
self.email_address = email_address
if not key:
key = request_email_verification_code(
context.request, user=email_address.user, email=email_address.email
)
self.key = key
@classmethod
def create(cls, email_address: EmailAddress):
return EmailVerificationModel(email_address)
@classmethod
def from_key(cls, key):
verification, _ = get_pending_verification(context.request, peek=True)
if not verification or not compare_code(actual=key, expected=verification.key):
return None
return verification
def key_expired(self):
return False
def clear_state(request):
request.session.pop(EMAIL_VERIFICATION_CODE_SESSION_KEY, None)
clear_login(request)
def request_email_verification_code(
request: HttpRequest,
user,
email: str,
) -> str:
code = ""
pending_verification = {
"at": time.time(),
"failed_attempts": 0,
"email": email,
}
pretend = user is None
if not pretend:
adapter = get_adapter()
code = adapter.generate_email_verification_code()
assert user._meta.pk
pending_verification.update(
{
"user_id": user._meta.pk.value_to_string(user),
"email": email,
"code": code,
}
)
request.session[EMAIL_VERIFICATION_CODE_SESSION_KEY] = pending_verification
return code
def get_pending_verification(
request: HttpRequest, peek: bool = False
) -> Tuple[Optional[EmailVerificationModel], Optional[Dict[str, Any]]]:
if peek:
data = request.session.get(EMAIL_VERIFICATION_CODE_SESSION_KEY)
else:
data = request.session.pop(EMAIL_VERIFICATION_CODE_SESSION_KEY, None)
if not data:
clear_state(request)
return None, None
if time.time() - data["at"] >= app_settings.EMAIL_VERIFICATION_BY_CODE_TIMEOUT:
clear_state(request)
return None, None
if user_id_str := data.get("user_id"):
user_id = get_user_model()._meta.pk.to_python(user_id_str) # type: ignore[union-attr]
user = get_user_model().objects.get(pk=user_id)
email = data["email"]
try:
email_address = EmailAddress.objects.get_for_user(user, email)
except EmailAddress.DoesNotExist:
email_address = EmailAddress(user=user, email=email)
verification = EmailVerificationModel(email_address, key=data["code"])
else:
verification = None
return verification, data
def record_invalid_attempt(
request: HttpRequest, pending_verification: Dict[str, Any]
) -> bool:
n = pending_verification["failed_attempts"]
n += 1
pending_verification["failed_attempts"] = n
if n >= app_settings.EMAIL_VERIFICATION_BY_CODE_MAX_ATTEMPTS:
clear_state(request)
return False
else:
request.session[EMAIL_VERIFICATION_CODE_SESSION_KEY] = pending_verification
return True

View File

@@ -0,0 +1,117 @@
import time
from typing import Any, Dict
from django.http import HttpRequest, HttpResponse
from allauth import app_settings as allauth_settings
from allauth.account.adapter import get_adapter
from allauth.account.models import Login
from allauth.core.exceptions import ImmediateHttpResponse
AUTHENTICATION_METHODS_SESSION_KEY = "account_authentication_methods"
def record_authentication(request, method, **extra_data):
"""Here we keep a log of all authentication methods used within the current
session. Important to note is that having entries here does not imply that
a user is fully signed in. For example, consider a case where a user
authenticates using a password, but fails to complete the 2FA challenge.
Or, a user successfully signs in into an inactive account or one that still
needs verification. In such cases, ``request.user`` is still anonymous, yet,
we do have an entry here.
Example data::
{'method': 'password',
'at': 1701423602.7184925,
'username': 'john.doe'}
{'method': 'socialaccount',
'at': 1701423567.6368647,
'provider': 'amazon',
'uid': 'amzn1.account.K2LI23KL2LK2'}
{'method': 'mfa',
'at': 1701423602.6392953,
'id': 1,
'type': 'totp'}
"""
methods = request.session.get(AUTHENTICATION_METHODS_SESSION_KEY, [])
data = {
"method": method,
"at": time.time(),
**extra_data,
}
methods.append(data)
request.session[AUTHENTICATION_METHODS_SESSION_KEY] = methods
def _get_login_hook_kwargs(login: Login) -> Dict[str, Any]:
"""
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 perform_password_login(
request: HttpRequest, credentials: Dict[str, Any], login: Login
) -> HttpResponse:
extra_data = {
field: credentials.get(field)
for field in ["email", "username"]
if credentials.get(field)
}
record_authentication(request, method="password", **extra_data)
return perform_login(request, login)
def perform_login(request: HttpRequest, login: Login) -> HttpResponse:
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 resume_login(request: HttpRequest, login: Login) -> HttpResponse:
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:
if allauth_settings.HEADLESS_ENABLED:
from allauth.headless.internal.restkit.response import APIResponse
if isinstance(e.response, APIResponse):
raise e
response = e.response
return response
def is_login_rate_limited(request, login: Login) -> bool:
from allauth.account.internal.flows.email_verification import (
is_verification_rate_limited,
)
if is_verification_rate_limited(request, login):
return True
return False

View File

@@ -0,0 +1,133 @@
import time
from typing import Any, Dict, Optional, Tuple
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from django.http import HttpRequest
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.internal.flows.email_verification import (
verify_email_indirectly,
)
from allauth.account.internal.flows.login import (
perform_login,
record_authentication,
)
from allauth.account.internal.flows.signup import send_unknown_account_mail
from allauth.account.internal.stagekit import clear_login, stash_login
from allauth.account.models import Login
LOGIN_CODE_STATE_KEY = "login_code"
def request_login_code(
request: HttpRequest, email: str, login: Optional[Login] = None
) -> None:
from allauth.account.utils import filter_users_by_email
initiated_by_user = login is None
adapter = get_adapter()
users = filter_users_by_email(email, is_active=True, prefer_verified=True)
pending_login = {
"at": time.time(),
"email": email,
"failed_attempts": 0,
"initiated_by_user": initiated_by_user,
}
if not users:
user = None
send_unknown_account_mail(request, email)
else:
user = users[0]
code = adapter.generate_login_code()
context = {
"request": request,
"code": code,
}
adapter.send_mail("account/email/login_code", email, context)
pending_login.update(
{"code": code, "user_id": user._meta.pk.value_to_string(user)}
)
adapter.add_message(
request,
messages.SUCCESS,
"account/messages/login_code_sent.txt",
{"email": email},
)
if initiated_by_user:
login = Login(user=user, email=email)
login.state["stages"] = {"current": "login_by_code"}
assert login
login.state[LOGIN_CODE_STATE_KEY] = pending_login
if initiated_by_user:
stash_login(request, login)
def get_pending_login(
request, login: Login, peek: bool = False
) -> Tuple[Optional[AbstractBaseUser], Optional[Dict[str, Any]]]:
if peek:
data = login.state.get(LOGIN_CODE_STATE_KEY)
else:
data = login.state.pop(LOGIN_CODE_STATE_KEY, None)
if not data:
return None, None
if time.time() - data["at"] >= app_settings.LOGIN_BY_CODE_TIMEOUT:
login.state.pop(LOGIN_CODE_STATE_KEY, None)
clear_login(request)
return None, None
user_id_str = data.get("user_id")
user = None
if user_id_str:
user_id = get_user_model()._meta.pk.to_python(user_id_str) # type: ignore[union-attr]
user = get_user_model().objects.get(pk=user_id)
return user, data
def record_invalid_attempt(request, login: Login) -> bool:
from allauth.account.internal.stagekit import stash_login, unstash_login
pending_login = login.state[LOGIN_CODE_STATE_KEY]
n = pending_login["failed_attempts"]
n += 1
pending_login["failed_attempts"] = n
if n >= app_settings.LOGIN_BY_CODE_MAX_ATTEMPTS:
unstash_login(request)
return False
else:
login.state[LOGIN_CODE_STATE_KEY] = pending_login
stash_login(request, login)
return True
def perform_login_by_code(
request: HttpRequest,
stage,
redirect_url: Optional[str],
):
state = stage.login.state[LOGIN_CODE_STATE_KEY]
email = state["email"]
user = stage.login.user
record_authentication(request, method="code", email=email)
verify_email_indirectly(request, user, email)
if state["initiated_by_user"]:
# Just requesting a login code does is not considered to be a real login,
# yet, is needed in order to make the stage machinery work. Now that we've
# completed the code, let's start a real login.
login = Login(
user=user,
redirect_url=redirect_url,
email=email,
)
return perform_login(request, login)
else:
return stage.exit()
def compare_code(*, actual, expected) -> bool:
actual = actual.replace(" ", "").lower()
expected = expected.replace(" ", "").lower()
return expected and actual == expected

View File

@@ -0,0 +1,16 @@
from django.contrib import messages
from django.http import HttpRequest
from allauth.account.internal.stagekit import clear_login
def logout(request: HttpRequest) -> None:
from allauth.account.adapter import get_adapter
if request.user.is_authenticated:
adapter = get_adapter()
adapter.add_message(
request, messages.SUCCESS, "account/messages/logged_out.txt"
)
adapter.logout(request)
clear_login(request)

View File

@@ -0,0 +1,173 @@
from typing import Optional
from django.contrib import messages
from django.http import HttpRequest
from allauth.account import app_settings, signals
from allauth.account.adapter import get_adapter
from allauth.account.internal.flows.reauthentication import (
raise_if_reauthentication_required,
)
from allauth.account.models import EmailAddress
def can_delete_email(email_address: EmailAddress) -> bool:
adapter = get_adapter()
return adapter.can_delete_email(email_address)
def delete_email(request: HttpRequest, email_address: EmailAddress) -> bool:
if app_settings.REAUTHENTICATION_REQUIRED:
raise_if_reauthentication_required(request)
success = False
adapter = get_adapter()
if not can_delete_email(email_address):
adapter.add_message(
request,
messages.ERROR,
"account/messages/cannot_delete_primary_email.txt",
{"email": email_address.email},
)
else:
email_address.remove()
signals.email_removed.send(
sender=EmailAddress,
request=request,
user=request.user,
email_address=email_address,
)
adapter.add_message(
request,
messages.SUCCESS,
"account/messages/email_deleted.txt",
{"email": email_address.email},
)
adapter.send_notification_mail(
"account/email/email_deleted",
request.user,
{"deleted_email": email_address.email},
)
success = True
return success
def add_email(request: HttpRequest, form):
if app_settings.REAUTHENTICATION_REQUIRED:
raise_if_reauthentication_required(request)
email_address = form.save(request)
adapter = get_adapter(request)
adapter.add_message(
request,
messages.INFO,
"account/messages/email_confirmation_sent.txt",
{"email": form.cleaned_data["email"]},
)
if email_address.pk:
signals.email_added.send(
sender=EmailAddress,
request=request,
user=request.user,
email_address=email_address,
)
def can_mark_as_primary(email_address: EmailAddress):
return (
email_address.verified
or not EmailAddress.objects.filter(
user=email_address.user, verified=True
).exists()
)
def mark_as_primary(request: HttpRequest, email_address: EmailAddress):
if app_settings.REAUTHENTICATION_REQUIRED:
raise_if_reauthentication_required(request)
# Not primary=True -- Slightly different variation, don't
# require verified unless moving from a verified
# address. Ignore constraint if previous primary email
# address is not verified.
success = False
if not can_mark_as_primary(email_address):
get_adapter().add_message(
request,
messages.ERROR,
"account/messages/unverified_primary_email.txt",
)
else:
assert request.user.is_authenticated
from_email_address = EmailAddress.objects.filter(
user=request.user, primary=True
).first()
email_address.set_as_primary()
adapter = get_adapter()
adapter.add_message(
request,
messages.SUCCESS,
"account/messages/primary_email_set.txt",
)
emit_email_changed(request, from_email_address, email_address)
success = True
return success
def emit_email_changed(request, from_email_address, to_email_address) -> None:
user = to_email_address.user
signals.email_changed.send(
sender=EmailAddress,
request=request,
user=user,
from_email_address=from_email_address,
to_email_address=to_email_address,
)
if from_email_address:
get_adapter().send_notification_mail(
"account/email/email_changed",
user,
context={
"from_email": from_email_address.email,
"to_email": to_email_address.email,
},
email=from_email_address.email,
)
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 allauth.account.utils import filter_users_by_email
if not filter_users_by_email(email):
# 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

View File

@@ -0,0 +1,55 @@
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import AbstractBaseUser
from django.http import HttpRequest
from allauth.account import app_settings, signals
from allauth.account.adapter import get_adapter
from allauth.account.internal.flows.logout import logout
def change_password(user: AbstractBaseUser, password: str) -> None:
get_adapter().set_password(user, password)
def finalize_password_change(request: HttpRequest, user: AbstractBaseUser) -> bool:
logged_out = logout_on_password_change(request, user)
adapter = get_adapter(request)
adapter.add_message(
request,
messages.SUCCESS,
"account/messages/password_changed.txt",
)
adapter.send_notification_mail("account/email/password_changed", user)
signals.password_changed.send(
sender=user.__class__,
request=request,
user=user,
)
return logged_out
def finalize_password_set(request: HttpRequest, user: AbstractBaseUser) -> bool:
logged_out = logout_on_password_change(request, user)
adapter = get_adapter(request)
adapter.add_message(request, messages.SUCCESS, "account/messages/password_set.txt")
adapter.send_notification_mail("account/email/password_set", user)
signals.password_set.send(
sender=user.__class__,
request=request,
user=user,
)
return logged_out
def logout_on_password_change(request: HttpRequest, user: AbstractBaseUser) -> bool:
# 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.
logged_out = True
if not app_settings.LOGOUT_ON_PASSWORD_CHANGE:
update_session_auth_hash(request, user)
logged_out = False
else:
logout(request)
return logged_out

View File

@@ -0,0 +1,62 @@
from urllib.parse import quote
from django.contrib import messages
from django.contrib.auth.models import AbstractBaseUser
from django.http import HttpRequest
from django.urls import reverse
from allauth.account import signals
from allauth.account.adapter import get_adapter
from allauth.account.models import EmailAddress
from allauth.core.internal.httpkit import get_frontend_url
from allauth.utils import build_absolute_uri
def reset_password(user: AbstractBaseUser, password: str) -> None:
get_adapter().set_password(user, password)
def finalize_password_reset(request: HttpRequest, user: AbstractBaseUser) -> None:
adapter = get_adapter()
if user:
# User successfully reset the password, clear any
# possible cache entries for all email addresses.
for email in EmailAddress.objects.filter(user_id=user.pk):
adapter._delete_login_attempts_cached_email(request, email=email.email)
adapter.add_message(
request,
messages.SUCCESS,
"account/messages/password_changed.txt",
)
signals.password_reset.send(
sender=user.__class__,
request=request,
user=user,
)
adapter.send_notification_mail("account/email/password_reset", user)
def get_reset_password_url(request: HttpRequest) -> str:
url = get_frontend_url(request, "account_reset_password")
if not url:
url = build_absolute_uri(request, reverse("account_reset_password"))
return url
def get_reset_password_from_key_url(request: HttpRequest, key: str) -> str:
"""
Method intented to be overriden in case the password reset email
needs to point to your frontend/SPA.
"""
url = get_frontend_url(request, "account_reset_password_from_key", key=key)
if not url:
# We intentionally accept an opaque `key` on the interface here, and not
# implementation details such as a separate `uidb36` and `key. Ideally,
# this should have done on `urls` level as well.
path = reverse(
"account_reset_password_from_key", kwargs={"uidb36": "UID", "key": "KEY"}
)
path = path.replace("UID-KEY", quote(key))
url = build_absolute_uri(request, path)
return url

View File

@@ -0,0 +1,108 @@
import time
from typing import Dict, List, Optional
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpRequest, HttpResponseRedirect
from django.urls import resolve, reverse
from django.utils.http import urlencode
from allauth import app_settings as allauth_settings
from allauth.account import app_settings
from allauth.account.authentication import get_authentication_records
from allauth.account.internal.flows.login import record_authentication
from allauth.core.exceptions import ReauthenticationRequired
from allauth.core.internal.httpkit import (
deserialize_request,
serialize_request,
)
from allauth.utils import import_callable
STATE_SESSION_KEY = "account_reauthentication_state"
def reauthenticate_by_password(request: HttpRequest) -> None:
record_authentication(request, method="password", reauthenticated=True)
def stash_and_reauthenticate(
request: HttpRequest, state: dict, callback: str
) -> HttpResponseRedirect:
request.session[STATE_SESSION_KEY] = {
"state": state,
"callback": callback,
}
return HttpResponseRedirect(reverse("account_reauthenticate"))
def suspend_request(request: HttpRequest, redirect_to: str) -> HttpResponseRedirect:
path = request.get_full_path()
if request.method == "POST":
request.session[STATE_SESSION_KEY] = {"request": serialize_request(request)}
return HttpResponseRedirect(
redirect_to + "?" + urlencode({REDIRECT_FIELD_NAME: path})
)
def resume_request(request: HttpRequest) -> Optional[HttpResponseRedirect]:
from allauth.account.utils import get_next_redirect_url
state = request.session.pop(STATE_SESSION_KEY, None)
if state and "callback" in state:
callback = import_callable(state["callback"])
return callback(request, state["state"])
url = get_next_redirect_url(request, REDIRECT_FIELD_NAME)
if not url:
return None
if state and "request" in state:
suspended_request = deserialize_request(state["request"], request)
if suspended_request.path == url:
resolved = resolve(suspended_request.path)
return resolved.func(suspended_request, *resolved.args, **resolved.kwargs)
return HttpResponseRedirect(url)
def raise_if_reauthentication_required(request: HttpRequest) -> None:
if not did_recently_authenticate(request):
raise ReauthenticationRequired()
def did_recently_authenticate(request: HttpRequest) -> bool:
if request.user.is_anonymous:
return False
if not get_reauthentication_flows(request.user):
# 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
methods = get_authentication_records(request)
if not methods:
return False
authenticated_at = methods[-1]["at"]
return time.time() - authenticated_at < app_settings.REAUTHENTICATION_TIMEOUT
def get_reauthentication_flows(user) -> List[Dict]:
ret: List[Dict] = []
if not user.is_authenticated:
return ret
if user.has_usable_password():
entry = {
"id": "reauthenticate",
}
ret.append(entry)
if allauth_settings.MFA_ENABLED:
from allauth.mfa.models import Authenticator
from allauth.mfa.utils import is_mfa_enabled
types = []
for typ in Authenticator.Type:
if is_mfa_enabled(user, types=[typ]):
types.append(typ)
if types:
ret.append({"id": "mfa_reauthenticate", "types": types})
return ret

View File

@@ -0,0 +1,72 @@
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from allauth.account import app_settings, signals
from allauth.account.adapter import get_adapter
from allauth.account.internal.flows import email_verification_by_code
from allauth.account.internal.flows.login import perform_login
from allauth.account.models import Login
from allauth.core.internal.httpkit import get_frontend_url
from allauth.utils import build_absolute_uri
def prevent_enumeration(request: HttpRequest, email: str) -> HttpResponse:
adapter = get_adapter(request)
adapter.send_account_already_exists_mail(email)
adapter.add_message(
request,
messages.INFO,
"account/messages/email_confirmation_sent.txt",
{"email": email, "login": False, "signup": True},
)
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
email_verification_by_code.request_email_verification_code(
request, user=None, email=email
)
resp = adapter.respond_email_verification_sent(request, None)
return resp
def send_unknown_account_mail(request: HttpRequest, email: str) -> None:
if not app_settings.EMAIL_UNKNOWN_ACCOUNTS:
return None
signup_url = get_signup_url(request)
context = {
"request": request,
"signup_url": signup_url,
}
get_adapter().send_mail("account/email/unknown_account", email, context)
def get_signup_url(request: HttpRequest) -> str:
url = get_frontend_url(request, "account_signup")
if not url:
url = build_absolute_uri(request, reverse("account_signup"))
return url
def complete_signup(
request,
*,
user,
email_verification=None,
redirect_url=None,
signal_kwargs=None,
by_passkey=False,
):
if signal_kwargs is None:
signal_kwargs = {}
signals.user_signed_up.send(
sender=user.__class__, request=request, user=user, **signal_kwargs
)
login = Login(
user=user,
email_verification=email_verification,
redirect_url=redirect_url,
signal_kwargs=signal_kwargs,
signup=True,
)
if by_passkey:
login.state["passkey_signup"] = True
return perform_login(request, login)

View File

@@ -0,0 +1,58 @@
import time
from typing import Optional
from django.http import HttpResponseRedirect
from django.urls import reverse
from allauth.account import app_settings
from allauth.account.models import Login
from allauth.account.stages import LoginStage, LoginStageController
LOGIN_SESSION_KEY = "account_login"
def get_pending_stage(request) -> Optional[LoginStage]:
stage = None
if not request.user.is_authenticated:
login = unstash_login(request, peek=True)
if login:
ctrl = LoginStageController(request, login)
stage = ctrl.get_pending_stage()
return stage
def redirect_to_pending_stage(request, stage: LoginStage):
if stage.urlname:
return HttpResponseRedirect(reverse(stage.urlname))
clear_login(request)
return HttpResponseRedirect(reverse("account_login"))
def clear_login(request):
request.session.pop(LOGIN_SESSION_KEY, None)
def unstash_login(request, peek=False):
login = None
if peek:
data = request.session.get(LOGIN_SESSION_KEY)
else:
data = request.session.pop(LOGIN_SESSION_KEY, None)
if isinstance(data, dict):
try:
login = Login.deserialize(data)
except ValueError:
pass
else:
if time.time() - login.initiated_at > app_settings.LOGIN_TIMEOUT:
login = None
clear_login(request)
else:
request._account_login_accessed = True
return login
def stash_login(request, login):
request.session[LOGIN_SESSION_KEY] = login.serialize()
request._account_login_accessed = True

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,136 @@
from datetime import timedelta
from typing import Optional
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)
email = email.lower()
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 = email.lower()
email_address, created = self.get_or_create(
user=user, email=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_primary_email(self, user) -> Optional[str]:
from allauth.account.utils import user_email
primary = self.get_primary(user)
if primary:
email = primary.email
else:
email = user_email(user)
return email
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=email.lower())
]
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)
email = email.lower()
if addresses is None:
ret = self.get(user=user, email=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 == email:
return address
raise self.model.DoesNotExist()
def is_verified(self, email):
return self.filter(email=email.lower(), verified=True).exists()
def lookup(self, emails):
return self.filter(email__in=[e.lower() for e in emails])
class EmailConfirmationManager(models.Manager):
def all_expired(self):
return self.filter(self.expired_q())
def all_valid(self):
return self.exclude(self.expired_q()).filter(email_address__verified=False)
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,93 @@
import os
from types import SimpleNamespace
from django.http import HttpResponseRedirect
from django.urls import NoReverseMatch, reverse
from django.utils.decorators import sync_and_async_middleware
from asgiref.sync import iscoroutinefunction
from allauth.account.adapter import get_adapter
from allauth.account.internal import flows
from allauth.core import context
from allauth.core.exceptions import (
ImmediateHttpResponse,
ReauthenticationRequired,
)
@sync_and_async_middleware
def AccountMiddleware(get_response):
if iscoroutinefunction(get_response):
async def middleware(request):
request.allauth = SimpleNamespace()
with context.request_context(request):
response = await get_response(request)
if _should_redirect_accounts(request, response):
response = await _aredirect_accounts(request)
return response
else:
def middleware(request):
request.allauth = SimpleNamespace()
with context.request_context(request):
response = get_response(request)
if _should_redirect_accounts(request, response):
response = _redirect_accounts(request)
return response
def process_exception(request, exception):
if isinstance(exception, ImmediateHttpResponse):
return exception.response
elif isinstance(exception, ReauthenticationRequired):
redirect_url = reverse("account_login")
methods = get_adapter().get_reauthentication_methods(request.user)
if methods:
redirect_url = methods[0]["url"]
return flows.reauthentication.suspend_request(request, redirect_url)
middleware.process_exception = process_exception
return middleware
def _should_redirect_accounts(request, response) -> bool:
"""
URLs should be hackable. Yet, assuming allauth is included like this...
path("accounts/", include("allauth.urls")),
... and a user would attempt to navigate to /accounts/, a 404 would be
presented. This code catches that 404, and redirects to either the email
management overview or the login page, depending on whether or not the user
is authenticated.
"""
if response.status_code != 404:
return False
try:
login_path = reverse("account_login")
email_path = reverse("account_email")
except NoReverseMatch:
# Project might have deviated URLs, let's keep out of the way.
return False
prefix = os.path.commonprefix([login_path, email_path])
if len(prefix) <= 1 or prefix != request.path:
return False
# If we have a prefix that is not just '/', and that is what our request is
# pointing to, redirect.
return True
async def _aredirect_accounts(request) -> HttpResponseRedirect:
email_path = reverse("account_email")
login_path = reverse("account_login")
user = await request.auser()
return HttpResponseRedirect(email_path if user.is_authenticated else login_path)
def _redirect_accounts(request) -> HttpResponseRedirect:
email_path = reverse("account_email")
login_path = reverse("account_login")
user = request.user
return HttpResponseRedirect(email_path if user.is_authenticated else login_path)

View File

@@ -0,0 +1,105 @@
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,32 @@
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,26 @@
from django.conf import settings
from django.db import migrations
from django.db.models.functions import Lower
from allauth.account import app_settings
def forwards(apps, schema_editor):
EmailAddress = apps.get_model("account.EmailAddress")
User = apps.get_model(settings.AUTH_USER_MODEL)
EmailAddress.objects.all().exclude(email=Lower("email")).update(
email=Lower("email")
)
email_field = app_settings.USER_MODEL_EMAIL_FIELD
if email_field:
User.objects.all().exclude(**{email_field: Lower(email_field)}).update(
**{email_field: Lower(email_field)}
)
class Migration(migrations.Migration):
dependencies = [
("account", "0005_emailaddress_idx_upper_email"),
]
operations = [migrations.RunPython(forwards)]

View File

@@ -0,0 +1,25 @@
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", "0006_emailaddress_lower"),
]
operations = [
migrations.RemoveIndex(
model_name="emailaddress",
name="account_emailaddress_upper",
),
migrations.AlterField(
model_name="emailaddress",
name="email",
field=models.EmailField(
db_index=True, max_length=EMAIL_MAX_LENGTH, verbose_name="email address"
),
),
]

View File

@@ -0,0 +1,44 @@
from django.conf import settings
from django.db import migrations
from django.db.models import Count
def forwards(apps, schema_editor):
EmailAddress = apps.get_model("account.EmailAddress")
User = apps.get_model(settings.AUTH_USER_MODEL)
user_email_field = getattr(settings, "ACCOUNT_USER_MODEL_EMAIL_FIELD", "email")
def get_users_with_multiple_primary_email():
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 User.objects.filter(pk__in=user_pks)
def unset_extra_primary_emails(user):
qs = EmailAddress.objects.filter(user=user, primary=True)
primary_email_addresses = list(qs)
if not primary_email_addresses:
return
primary_email_address = primary_email_addresses[0]
if user_email_field:
for address in primary_email_addresses:
if address.email.lower() == getattr(user, user_email_field, "").lower():
primary_email_address = address
break
qs.exclude(pk=primary_email_address.pk).update(primary=False)
for user in get_users_with_multiple_primary_email().iterator():
unset_extra_primary_emails(user)
class Migration(migrations.Migration):
dependencies = [
("account", "0007_emailaddress_idx_email"),
]
operations = [migrations.RunPython(forwards)]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.11 on 2024-05-09 06:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("account", "0008_emailaddress_unique_primary_email_fixup"),
]
operations = [
migrations.AddConstraint(
model_name="emailaddress",
constraint=models.UniqueConstraint(
condition=models.Q(("primary", True)),
fields=("user", "primary"),
name="unique_primary_email",
),
),
]

View File

@@ -0,0 +1,193 @@
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
from django.utils.decorators import method_decorator
from django.utils.html import format_html
from django.views.decorators.cache import never_cache
from allauth.account import app_settings
from allauth.account.adapter import get_adapter
from allauth.account.internal import flows
from allauth.account.internal.decorators import login_not_required
from allauth.account.internal.stagekit import (
get_pending_stage,
redirect_to_pending_stage,
)
from allauth.account.utils import (
get_login_redirect_url,
get_next_redirect_url,
passthrough_next_redirect_url,
)
from allauth.core.exceptions import ImmediateHttpResponse
from allauth.utils import get_request_param
def _ajax_response(request, response, form=None, data=None):
adapter = get_adapter()
if adapter.is_ajax(request):
if isinstance(response, HttpResponseRedirect) or isinstance(
response, HttpResponsePermanentRedirect
):
redirect_to = response["Location"]
else:
redirect_to = None
response = adapter.ajax_response(
request, response, form=form, data=data, redirect_to=redirect_to
)
return response
class RedirectAuthenticatedUserMixin:
@method_decorator(login_not_required)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if app_settings.AUTHENTICATED_LOGIN_REDIRECTS:
if request.user.is_authenticated:
redirect_to = self.get_authenticated_redirect_url()
response = HttpResponseRedirect(redirect_to)
return _ajax_response(request, response)
else:
stage = get_pending_stage(request)
if stage and stage.is_resumable(request):
return redirect_to_pending_stage(request, stage)
response = super().dispatch(request, *args, **kwargs)
return response
def get_authenticated_redirect_url(self):
redirect_field_name = self.redirect_field_name
return get_login_redirect_url(
self.request,
url=self.get_success_url(),
redirect_field_name=redirect_field_name,
)
class LogoutFunctionalityMixin:
def logout(self):
flows.logout.logout(self.request)
class AjaxCapableProcessFormViewMixin:
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
form = self.get_form()
return _ajax_response(
self.request, response, form=form, data=self._get_ajax_data_if()
)
def post(self, request, *args, **kwargs):
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
response = self.form_valid(form)
else:
response = self.form_invalid(form)
return _ajax_response(
self.request, response, form=form, data=self._get_ajax_data_if()
)
def get_form(self, form_class=None):
form = getattr(self, "_cached_form", None)
if form is None:
form = super().get_form(form_class)
self._cached_form = form
return form
def _get_ajax_data_if(self):
return (
self.get_ajax_data()
if get_adapter(self.request).is_ajax(self.request)
else None
)
def get_ajax_data(self):
return None
class CloseableSignupMixin:
template_name_signup_closed = (
"account/signup_closed." + app_settings.TEMPLATE_EXTENSION
)
def dispatch(self, request, *args, **kwargs):
try:
if not self.is_open():
return self.closed()
except ImmediateHttpResponse as e:
return e.response
return super().dispatch(request, *args, **kwargs)
def is_open(self):
return get_adapter(self.request).is_open_for_signup(self.request)
def closed(self):
response_kwargs = {
"request": self.request,
"template": self.template_name_signup_closed,
}
return self.response_class(**response_kwargs)
class NextRedirectMixin:
redirect_field_name = REDIRECT_FIELD_NAME
def get_context_data(self, **kwargs):
ret = super().get_context_data(**kwargs)
redirect_field_value = get_request_param(self.request, self.redirect_field_name)
ret.update(
{
"redirect_field_name": self.redirect_field_name,
"redirect_field_value": redirect_field_value,
"redirect_field": (
format_html(
'<input type="hidden" name="{}" value="{}">',
self.redirect_field_name,
redirect_field_value,
)
if redirect_field_value
else ""
),
}
)
return ret
def get_success_url(self):
"""
We're in a mixin, so we cannot rely on the fact that our super() has a get_success_url.
Also, we want to check for -- in this order:
1) The `?next=/foo`
2) The `get_succes_url()` if available.
3) The `.success_url` if available.
4) A fallback default success URL: `get_default_success_url()`.
"""
url = self.get_next_url()
if url:
return url
if not url:
if hasattr(super(), "get_success_url"):
try:
url = super().get_success_url()
except ImproperlyConfigured:
# Django's default get_success_url() checks self.succes_url,
# and throws this if that is not set. Yet, in our case, we
# want to fallback to the default.
pass
elif hasattr(self, "success_url"):
url = self.success_url
if url:
url = str(url) # reverse_lazy
if not url:
url = self.get_default_success_url()
return url
def get_default_success_url(self):
return None
def get_next_url(self):
return get_next_redirect_url(self.request, self.redirect_field_name)
def passthrough_next_url(self, url):
return passthrough_next_redirect_url(
self.request, url, self.redirect_field_name
)

View File

@@ -0,0 +1,335 @@
import datetime
import time
from typing import Dict, Optional
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from django.core import signing
from django.db import models
from django.db.models import Q
from django.db.models.constraints import UniqueConstraint
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from allauth.account import app_settings, signals
from allauth.account.adapter import get_adapter
from allauth.account.managers import (
EmailAddressManager,
EmailConfirmationManager,
)
class EmailAddress(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_("user"),
on_delete=models.CASCADE,
)
email = models.EmailField(
db_index=True,
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")]
constraints = [
UniqueConstraint(
fields=["user", "primary"],
name="unique_primary_email",
condition=Q(primary=True),
)
]
if app_settings.UNIQUE_EMAIL:
constraints.append(
UniqueConstraint(
fields=["email"],
name="unique_verified_email",
condition=Q(verified=True),
)
)
def __str__(self):
return self.email
def clean(self):
super().clean()
self.email = self.email.lower()
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=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):
model = get_emailconfirmation_model()
confirmation = model.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:
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)
@classmethod
def from_key(cls, key):
qs = EmailConfirmation.objects.all_valid()
qs = qs.select_related("email_address__user")
emailconfirmation = qs.filter(key=key.lower()).first()
return emailconfirmation
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 # type: ignore[attr-defined]
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):
def __init__(self, email_address):
self.email_address = email_address
@classmethod
def create(cls, email_address):
return EmailConfirmationHMAC(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
def key_expired(self):
return False
class Login:
"""
Represents a user that is in the process of logging in.
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).
"""
# Optional, because we might be prentending logins to prevent user
# enumeration.
user: Optional[AbstractBaseUser]
email_verification: app_settings.EmailVerificationMethod
signal_kwargs: Optional[Dict]
signup: bool
email: Optional[str]
state: Dict
initiated_at: float
redirect_url: Optional[str]
def __init__(
self,
user,
email_verification: Optional[app_settings.EmailVerificationMethod] = None,
redirect_url: Optional[str] = None,
signal_kwargs: Optional[Dict] = None,
signup: bool = False,
email: Optional[str] = None,
state: Optional[Dict] = None,
initiated_at: Optional[float] = None,
):
self.user = user
if not email_verification:
email_verification = app_settings.EMAIL_VERIFICATION
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
self.initiated_at = initiated_at if initiated_at else time.time()
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) if self.user else None,
"email_verification": self.email_verification,
"signup": self.signup,
"redirect_url": self.redirect_url,
"email": self.email,
"signal_kwargs": signal_kwargs,
"state": self.state,
"initiated_at": self.initiated_at,
}
return data
@classmethod
def deserialize(cls, data):
from allauth.account.utils import url_str_to_user_pk
user = None
user_pk = data["user_pk"]
if user_pk is not None:
user = (
get_user_model().objects.filter(pk=url_str_to_user_pk(user_pk)).first()
)
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:
from allauth.socialaccount.models import SocialLogin
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"],
initiated_at=data["initiated_at"],
)
except KeyError:
raise ValueError()
def get_emailconfirmation_model():
if app_settings.EMAIL_VERIFICATION_BY_CODE_ENABLED:
from allauth.account.internal.flows.email_verification_by_code import (
EmailVerificationModel,
)
return EmailVerificationModel
elif app_settings.EMAIL_CONFIRMATION_HMAC:
model = EmailConfirmationHMAC
else:
model = EmailConfirmation
return model

View File

@@ -0,0 +1,14 @@
import warnings
from allauth.account.internal.flows.reauthentication import (
did_recently_authenticate,
raise_if_reauthentication_required,
)
__all__ = [
"raise_if_reauthentication_required",
"did_recently_authenticate",
]
warnings.warn("allauth.account.reauthentication is deprecated")

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