Commit 58ab8f0a authored by Pierre-Elliott Bécue's avatar Pierre-Elliott Bécue

New upstream version 0.35.0

parent 80ce46c3
......@@ -27,6 +27,7 @@ Björn Andersson
Bojan Mihelac
Chris Beaven
Chris Davis
Christian Carter
Christopher Grebs
Daniel Eriksson
Daniel Widerin
......@@ -52,6 +53,7 @@ Jakob Gerhard Martinussen
James Rivett-Carnac
James Thompson
Jannis Leidel
Jannis Vajen
Jeff Triplett
Jerome Leclanche
Joe Vanderstelt
......
0.35.0 (2017-02-02)
*******************
Security notice
---------------
- As an extra security measure on top of what the standard Django password reset
token generator is already facilitating, allauth now adds the user email
address to the hash such that whenever the user's email address changes the
token is invalidated.
Note worthy changes
-------------------
- New provider: Azure, Microsoft Graph, Salesforce, Yahoo.
0.34.0 (2017-10-29)
*******************
......
Metadata-Version: 1.1
Name: django-allauth
Version: 0.34.0
Version: 0.35.0
Summary: Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication.
Home-page: http://github.com/pennersr/django-allauth
Author: Raymond Penners
......@@ -10,16 +10,16 @@ Description: ==========================
Welcome to django-allauth!
==========================
.. image:: https://badge.fury.io/py/django-allauth.png
.. image:: https://badge.fury.io/py/django-allauth.svg
:target: http://badge.fury.io/py/django-allauth
.. image:: https://travis-ci.org/pennersr/django-allauth.png
.. image:: https://travis-ci.org/pennersr/django-allauth.svg
:target: http://travis-ci.org/pennersr/django-allauth
.. image:: https://img.shields.io/pypi/v/django-allauth.svg
:target: https://pypi.python.org/pypi/django-allauth
.. image:: https://coveralls.io/repos/pennersr/django-allauth/badge.png?branch=master
.. image:: https://coveralls.io/repos/pennersr/django-allauth/badge.svg?branch=master
:alt: Coverage Status
:target: https://coveralls.io/r/pennersr/django-allauth
......@@ -103,11 +103,9 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Framework :: Django
Classifier: Framework :: Django :: 1.8
Classifier: Framework :: Django :: 1.10
Classifier: Framework :: Django :: 1.11
Classifier: Framework :: Django :: 2.0
......@@ -2,16 +2,16 @@
Welcome to django-allauth!
==========================
.. image:: https://badge.fury.io/py/django-allauth.png
.. image:: https://badge.fury.io/py/django-allauth.svg
:target: http://badge.fury.io/py/django-allauth
.. image:: https://travis-ci.org/pennersr/django-allauth.png
.. image:: https://travis-ci.org/pennersr/django-allauth.svg
:target: http://travis-ci.org/pennersr/django-allauth
.. image:: https://img.shields.io/pypi/v/django-allauth.svg
:target: https://pypi.python.org/pypi/django-allauth
.. image:: https://coveralls.io/repos/pennersr/django-allauth/badge.png?branch=master
.. image:: https://coveralls.io/repos/pennersr/django-allauth/badge.svg?branch=master
:alt: Coverage Status
:target: https://coveralls.io/r/pennersr/django-allauth
......
"""
r"""
_ ___ __ __ .___________. __ __
/\| |/\ / \ | | | | | || | | |
\ ` ' / / ^ \ | | | | `---| |----`| |__| |
......@@ -8,7 +8,7 @@
"""
VERSION = (0, 34, 0, 'final', 0)
VERSION = (0, 35, 0, 'final', 0)
__title__ = 'django-allauth'
__version_info__ = VERSION
......@@ -16,4 +16,4 @@ __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-2017 Raymond Penners and contributors'
__copyright__ = 'Copyright 2010-2018 Raymond Penners and contributors'
......@@ -9,11 +9,13 @@ from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
authenticate,
get_backends,
login as django_login,
logout as django_logout,
)
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.password_validation import validate_password
from django.contrib.sites.shortcuts import get_current_site
from django.core.cache import cache
from django.core.mail import EmailMessage, EmailMultiAlternatives
......@@ -21,11 +23,12 @@ 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.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from . import app_settings
from ..compat import authenticate, is_authenticated, reverse, validate_password
from ..utils import (
build_absolute_uri,
email_address_exists,
......@@ -35,12 +38,6 @@ from ..utils import (
)
try:
from django.utils.encoding import force_text
except ImportError:
from django.utils.encoding import force_unicode as force_text
class DefaultAccountAdapter(object):
error_messages = {
......@@ -145,7 +142,7 @@ class DefaultAccountAdapter(object):
that URLs passed explicitly (e.g. by passing along a `next`
GET parameter) take precedence over the value returned here.
"""
assert is_authenticated(request.user)
assert request.user.is_authenticated
url = getattr(settings, "LOGIN_REDIRECT_URLNAME", None)
if url:
warnings.warn("LOGIN_REDIRECT_URLNAME is deprecated, simply"
......@@ -168,7 +165,7 @@ class DefaultAccountAdapter(object):
"""
The URL to return to after successful e-mail confirmation.
"""
if is_authenticated(request.user):
if request.user.is_authenticated:
if app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL:
return \
app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL
......@@ -480,7 +477,7 @@ class DefaultAccountAdapter(object):
def authenticate(self, request, **credentials):
"""Only authenticates, does not actually login. See `login`"""
self.pre_authenticate(request, **credentials)
user = authenticate(request=request, **credentials)
user = authenticate(request, **credentials)
if user:
cache_key = self._get_login_attempts_cache_key(
request, **credentials)
......@@ -496,6 +493,9 @@ class DefaultAccountAdapter(object):
data.append(time.mktime(dt.timetuple()))
cache.set(cache_key, data, app_settings.LOGIN_ATTEMPTS_TIMEOUT)
def is_ajax(self, request):
return request.is_ajax()
def get_adapter(request=None):
return import_attribute(app_settings.ADAPTER)(request)
import django
from django.contrib import admin
from . import app_settings
......@@ -12,11 +11,6 @@ class EmailAddressAdmin(admin.ModelAdmin):
search_fields = []
raw_id_fields = ('user',)
def __init__(self, *args, **kwargs):
super(EmailAddressAdmin, self).__init__(*args, **kwargs)
if not self.search_fields and django.VERSION[:2] < (1, 7):
self.search_fields = self.get_search_fields(None)
def get_search_fields(self, request):
base_fields = get_adapter(request).get_user_search_fields()
return ['email'] + list(map(lambda a: 'user__' + a, base_fields))
......
......@@ -141,13 +141,9 @@ class AppSettings(object):
"""
Minimum password Length
"""
import django
from django.conf import settings
ret = None
has_validators = (
django.VERSION[:2] >= (1, 9) and
bool(getattr(settings, 'AUTH_PASSWORD_VALIDATORS', [])))
if not has_validators:
if not settings.AUTH_PASSWORD_VALIDATORS:
ret = self._setting("PASSWORD_MIN_LENGTH", 6)
return ret
......
......@@ -8,7 +8,7 @@ from .utils import filter_users_by_email, filter_users_by_username
class AuthenticationBackend(ModelBackend):
def authenticate(self, **credentials):
def authenticate(self, request, **credentials):
ret = None
if app_settings.AUTHENTICATION_METHOD == AuthenticationMethod.EMAIL:
ret = self._authenticate_by_email(**credentials)
......
......@@ -4,13 +4,13 @@ import warnings
from importlib import import_module
from django import forms
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.sites.shortcuts import get_current_site
from django.core import exceptions, validators
from django.urls import reverse
from django.utils.translation import pgettext, ugettext, ugettext_lazy as _
from . import app_settings
from ..compat import reverse
from ..utils import (
build_absolute_uri,
get_username_max_length,
......@@ -24,6 +24,7 @@ from .utils import (
get_user_model,
perform_login,
setup_user_email,
sync_user_email_addresses,
url_str_to_user_pk,
user_email,
user_pk_to_url_str,
......@@ -31,6 +32,25 @@ from .utils import (
)
class EmailAwarePasswordResetTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
ret = super(
EmailAwarePasswordResetTokenGenerator, self)._make_hash_value(
user, timestamp)
sync_user_email_addresses(user)
emails = set([user.email])
emails.update(
EmailAddress.objects
.filter(user=user)
.values_list('email', flat=True))
ret += '|'.join(sorted(emails))
return ret
default_token_generator = EmailAwarePasswordResetTokenGenerator()
class PasswordVerificationMixin(object):
def clean(self):
cleaned_data = super(PasswordVerificationMixin, self).clean()
......@@ -50,7 +70,7 @@ class PasswordField(forms.CharField):
app_settings.PASSWORD_INPUT_RENDER_VALUE)
kwargs['widget'] = forms.PasswordInput(render_value=render_value,
attrs={'placeholder':
_(kwargs.get("label"))})
kwargs.get("label")})
super(PasswordField, self).__init__(*args, **kwargs)
......@@ -353,7 +373,7 @@ class SignupForm(BaseSignupForm):
def clean(self):
super(SignupForm, self).clean()
# `password` cannot by of type `SetPasswordField`, as we don't
# `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 validaton.
dummy_user = get_user_model()
......@@ -559,8 +579,11 @@ class UserTokenForm(forms.Form):
def clean(self):
cleaned_data = super(UserTokenForm, self).clean()
uidb36 = cleaned_data['uidb36']
key = cleaned_data['key']
uidb36 = cleaned_data.get('uidb36', None)
key = cleaned_data.get('key', None)
if not key:
raise forms.ValidationError(self.error_messages['token_invalid'])
self.reset_user = self._get_user(uidb36)
if (self.reset_user is None or
......
......@@ -4,7 +4,6 @@ import json
import uuid
from datetime import timedelta
import django
from django import forms
from django.conf import settings
from django.contrib.auth.models import AbstractUser, AnonymousUser
......@@ -14,6 +13,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.test.client import Client, RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.timezone import now
from allauth.account.forms import BaseSignupForm, SignupForm
......@@ -26,7 +26,6 @@ from allauth.tests import Mock, TestCase, patch
from allauth.utils import get_user_model, get_username_max_length
from . import app_settings
from ..compat import is_authenticated, reverse
from .adapter import get_adapter
from .auth_backends import AuthenticationBackend
from .signals import user_logged_out
......@@ -135,7 +134,7 @@ class AccountTests(TestCase):
@override_settings(
ACCOUNT_USERNAME_REQUIRED=True,
ACCOUNT_SIGNUP_PASSOWRD_ENTER_TWICE=True)
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE=True)
def test_signup_password_twice_form_error(self):
resp = self.client.post(
reverse('account_signup'),
......@@ -185,7 +184,7 @@ class AccountTests(TestCase):
def _create_user_and_login(self, usable_password=True):
password = 'doe' if usable_password else False
user = self._create_user(password=password)
self.client_force_login(user)
self.client.force_login(user)
return user
def test_redirect_when_authenticated(self):
......@@ -284,6 +283,36 @@ class AccountTests(TestCase):
self.assertEqual(mail.outbox[0].to, ["john@example.org"])
return user
def test_password_reset_flow_with_empty_session(self):
"""
Test the password reset flow when the session is empty:
requesting a new password, receiving the reset link via email,
following the link, getting redirected to the
new link (without the token)
Copying the link and using it in a DIFFERENT client (Browser/Device).
"""
# Request new password
self._request_new_password()
body = mail.outbox[0].body
self.assertGreater(body.find('https://'), 0)
# Extract URL for `password_reset_from_key` view
url = body[body.find('/password/reset/'):].split()[0]
resp = self.client.get(url)
reset_pass_url = resp.url
# Accesing the url via a different session
resp = self.client_class().get(reset_pass_url)
# We should receive the token_fail context_data
self.assertTemplateUsed(
resp,
'account/password_reset_from_key.%s' %
app_settings.TEMPLATE_EXTENSION)
self.assertTrue(resp.context_data['token_fail'])
def test_password_reset_flow(self):
"""
Tests the password reset flow: requesting a new password,
......@@ -349,6 +378,26 @@ class AccountTests(TestCase):
data = json.loads(response.content.decode('utf8'))
assert 'invalid' in data['form']['errors'][0]
def test_password_reset_flow_with_email_changed(self):
"""
Test that the password reset token is invalidated if
the user email address was changed.
"""
user = self._request_new_password()
body = mail.outbox[0].body
self.assertGreater(body.find('https://'), 0)
EmailAddress.objects.create(
user=user,
email='other@email.org')
# Extract URL for `password_reset_from_key` view
url = body[body.find('/password/reset/'):].split()[0]
resp = self.client.get(url)
self.assertTemplateUsed(
resp,
'account/password_reset_from_key.%s' %
app_settings.TEMPLATE_EXTENSION)
self.assertTrue('token_fail' in resp.context_data)
@override_settings(ACCOUNT_LOGIN_ON_PASSWORD_RESET=True)
def test_password_reset_ACCOUNT_LOGIN_ON_PASSWORD_RESET(self):
user = self._request_new_password()
......@@ -361,7 +410,7 @@ class AccountTests(TestCase):
resp.url,
{'password1': 'newpass123',
'password2': 'newpass123'})
self.assertTrue(is_authenticated(user))
self.assertTrue(user.is_authenticated)
# EmailVerificationMethod.MANDATORY sends us to the confirm-email page
self.assertRedirects(resp, '/confirm-email/')
......@@ -656,8 +705,6 @@ class AccountTests(TestCase):
}
}])
def test_django_password_validation(self):
if django.VERSION < (1, 9, ):
return
resp = self.client.post(
reverse('account_signup'),
{'username': 'johndoe',
......@@ -986,11 +1033,13 @@ class AuthenticationBackendTests(TestCase):
backend = AuthenticationBackend()
self.assertEqual(
backend.authenticate(
request=None,
username=user.username,
password=user.username).pk,
user.pk)
self.assertEqual(
backend.authenticate(
request=None,
username=user.email,
password=user.username),
None)
......@@ -1002,11 +1051,13 @@ class AuthenticationBackendTests(TestCase):
backend = AuthenticationBackend()
self.assertEqual(
backend.authenticate(
request=None,
username=user.email,
password=user.username).pk,
user.pk)
self.assertEqual(
backend.authenticate(
request=None,
username=user.username,
password=user.username),
None)
......@@ -1018,11 +1069,13 @@ class AuthenticationBackendTests(TestCase):
backend = AuthenticationBackend()
self.assertEqual(
backend.authenticate(
request=None,
username=user.email,
password=user.username).pk,
user.pk)
self.assertEqual(
backend.authenticate(
request=None,
username=user.username,
password=user.username).pk,
user.pk)
......
......@@ -9,6 +9,7 @@ from django.db import models
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.utils import six
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.timezone import now
......@@ -26,12 +27,6 @@ from .adapter import get_adapter
from .app_settings import EmailVerificationMethod
try:
from django.utils.encoding import force_text
except ImportError:
from django.utils.encoding import force_unicode as force_text
def get_next_redirect_url(request, redirect_field_name="next"):
"""
Returns the next URL to redirect to, if it was explicitly passed
......@@ -422,8 +417,8 @@ def url_str_to_user_pk(s):
User = get_user_model()
# TODO: Ugh, isn't there a cleaner way to determine whether or not
# the PK is a str-like field?
if getattr(User._meta.pk, 'rel', None):
pk_field = User._meta.pk.rel.to._meta.pk
if getattr(User._meta.pk, 'remote_field', None):
pk_field = User._meta.pk.remote_field.to._meta.pk
else:
pk_field = User._meta.pk
if issubclass(type(pk_field), models.UUIDField):
......
......@@ -7,13 +7,13 @@ from django.http import (
HttpResponseRedirect,
)
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import TemplateResponseMixin, TemplateView, View
from django.views.generic.edit import FormView
from . import app_settings, signals
from ..compat import is_anonymous, is_authenticated, reverse, reverse_lazy
from ..exceptions import ImmediateHttpResponse
from ..utils import get_form_class, get_request_param
from .adapter import get_adapter
......@@ -49,13 +49,14 @@ sensitive_post_parameters_m = method_decorator(
def _ajax_response(request, response, form=None, data=None):
if request.is_ajax():
adapter = get_adapter(request)
if adapter.is_ajax(request):
if (isinstance(response, HttpResponseRedirect) or isinstance(
response, HttpResponsePermanentRedirect)):
redirect_to = response['Location']
else:
redirect_to = None
response = get_adapter(request).ajax_response(
response = adapter.ajax_response(
request,
response,
form=form,
......@@ -67,7 +68,7 @@ def _ajax_response(request, response, form=None, data=None):
class RedirectAuthenticatedUserMixin(object):
def dispatch(self, request, *args, **kwargs):
if is_authenticated(request.user) and \
if request.user.is_authenticated and \
app_settings.AUTHENTICATED_LOGIN_REDIRECTS:
redirect_to = self.get_authenticated_redirect_url()
response = HttpResponseRedirect(redirect_to)
......@@ -114,7 +115,10 @@ class AjaxCapableProcessFormViewMixin(object):
return form
def _get_ajax_data_if(self):
return self.get_ajax_data() if self.request.is_ajax() else None
return (
self.get_ajax_data()
if get_adapter(self.request).is_ajax(self.request)
else None)
def get_ajax_data(self):
return None
......@@ -323,7 +327,7 @@ class ConfirmEmailView(TemplateResponseMixin, View):
if user_pk_str:
user_pk = url_str_to_user_pk(user_pk_str)
user = confirmation.email_address.user
if user_pk == user.pk and is_anonymous(self.request.user):
if user_pk == user.pk and self.request.user.is_anonymous:
return perform_login(self.request,
user,
app_settings.EmailVerificationMethod.NONE,
......@@ -759,14 +763,14 @@ class LogoutView(TemplateResponseMixin, View):
def get(self, *args, **kwargs):
if app_settings.LOGOUT_ON_GET:
return self.post(*args, **kwargs)
if not is_authenticated(self.request.user):
if not self.request.user.is_authenticated:
return redirect(self.get_redirect_url())
ctx = self.get_context_data()
return self.render_to_response(ctx)
def post(self, *args, **kwargs):
url = self.get_redirect_url()
if is_authenticated(self.request.user):
if self.request.user.is_authenticated:
self.logout()
return redirect(url)
......
import django
from django.utils import six
......@@ -7,52 +6,11 @@ try:
except ImportError:
from UserDict import UserDict # noqa
if django.VERSION > (1, 10,):
from django.urls import NoReverseMatch, reverse, reverse_lazy
else:
from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy # noqa
try:
from urllib.parse import parse_qsl, parse_qs, urlparse, urlunparse, urljoin
except ImportError:
from urlparse import parse_qsl, parse_qs, urlparse, urlunparse, urljoin # noqa
if django.VERSION >= (1, 9, 0):
from django.contrib.auth.password_validation import validate_password
else:
def validate_password(password, user=None, password_validators=None):
pass
def template_context_value(context, key):
try:
value = context[key]
except KeyError:
value = getattr(context, key)
return value
def is_anonymous(user):
if django.VERSION > (1, 10,):
return user.is_anonymous
else:
return user.is_anonymous()
def is_authenticated(user):
if django.VERSION > (1, 10,):
return user.is_authenticated
else:
return user.is_authenticated()
def authenticate(request=None, **credentials):
from django.contrib.auth import authenticate
if django.VERSION >= (1, 11, 0):
return authenticate(request=request, **credentials)
else:
return authenticate(**credentials)
def int_to_base36(i):
"""
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-29 09:56-0500\n"
"POT-Creation-Date: 2018-02-02 12:55-0600\n"
"PO-Revision-Date: 2016-01-19 19:32+0100\n"