Fix #1039: setting to enforce email signup verification

This commit is contained in:
Eliot Berriot 2020-04-01 14:34:56 +02:00
commit 93f2c9f83c
No known key found for this signature in database
GPG key ID: 6B501DFD73514E14
16 changed files with 365 additions and 30 deletions

View file

@ -1,3 +1,4 @@
from django.conf import settings
from django.utils.encoding import smart_text
from django.utils.translation import ugettext as _
from rest_framework import exceptions
@ -5,7 +6,48 @@ from rest_framework_jwt import authentication
from rest_framework_jwt.settings import api_settings
class JSONWebTokenAuthenticationQS(authentication.BaseJSONWebTokenAuthentication):
def should_verify_email(user):
if user.is_superuser:
return False
has_unverified_email = not user.has_verified_primary_email
mandatory_verification = settings.ACCOUNT_EMAIL_VERIFICATION != "optional"
return has_unverified_email and mandatory_verification
class BaseJsonWebTokenAuth(object):
def authenticate_credentials(self, payload):
"""
We have to implement this method by hand to ensure we can check that the
User has a verified email, if required
"""
User = authentication.get_user_model()
username = authentication.jwt_get_username_from_payload(payload)
if not username:
msg = _("Invalid payload.")
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
msg = _("Invalid signature.")
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = _("User account is disabled.")
raise exceptions.AuthenticationFailed(msg)
if should_verify_email(user):
msg = _("You need to verify your email address.")
raise exceptions.AuthenticationFailed(msg)
return user
class JSONWebTokenAuthenticationQS(
BaseJsonWebTokenAuth, authentication.BaseJSONWebTokenAuthentication
):
www_authenticate_realm = "api"
@ -22,7 +64,9 @@ class JSONWebTokenAuthenticationQS(authentication.BaseJSONWebTokenAuthentication
)
class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication):
class BearerTokenHeaderAuth(
BaseJsonWebTokenAuth, authentication.BaseJSONWebTokenAuthentication
):
"""
For backward compatibility purpose, we used Authorization: JWT <token>
but Authorization: Bearer <token> is probably better.
@ -65,7 +109,9 @@ class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication):
return auth
class JSONWebTokenAuthentication(authentication.JSONWebTokenAuthentication):
class JSONWebTokenAuthentication(
BaseJsonWebTokenAuth, authentication.JSONWebTokenAuthentication
):
def authenticate(self, request):
auth = super().authenticate(request)

View file

@ -3,6 +3,7 @@ import hashlib
from rest_framework import authentication, exceptions
from funkwhale_api.common import authentication as common_authentication
from funkwhale_api.users.models import User
@ -18,19 +19,26 @@ def authenticate(username, password):
if password.startswith("enc:"):
password = password.replace("enc:", "", 1)
password = binascii.unhexlify(password).decode("utf-8")
user = User.objects.select_related("actor").get(
username__iexact=username, is_active=True, subsonic_api_token=password
user = (
User.objects.all()
.for_auth()
.get(username__iexact=username, is_active=True, subsonic_api_token=password)
)
except (User.DoesNotExist, binascii.Error):
raise exceptions.AuthenticationFailed("Wrong username or password.")
if common_authentication.should_verify_email(user):
raise exceptions.AuthenticationFailed("You need to verify your email.")
return (user, None)
def authenticate_salt(username, salt, token):
try:
user = User.objects.select_related("actor").get(
username=username, is_active=True, subsonic_api_token__isnull=False
user = (
User.objects.all()
.for_auth()
.get(username=username, is_active=True, subsonic_api_token__isnull=False)
)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed("Wrong username or password.")
@ -38,6 +46,9 @@ def authenticate_salt(username, salt, token):
if expected != token:
raise exceptions.AuthenticationFailed("Wrong username or password.")
if common_authentication.should_verify_email(user):
raise exceptions.AuthenticationFailed("You need to verify your email.")
return (user, None)

View file

@ -22,3 +22,6 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter):
def send_mail(self, template_prefix, email, context):
context.update(get_email_context())
return super().send_mail(template_prefix, email, context)
def get_login_redirect_url(self, request):
return "noop"

View file

@ -1,4 +1,33 @@
from django.contrib.auth import backends, get_user_model
from allauth.account import auth_backends
from funkwhale_api.common import authentication
# ugly but allauth doesn't offer an easy way to override the querysets
# used to retrieve users, so we monkey patch
def decorate_for_auth(func):
def inner(*args, **kwargs):
qs = func(*args, **kwargs)
try:
return qs.for_auth()
except AttributeError:
return (
get_user_model()
.objects.all()
.for_auth()
.filter(pk__in=[u.pk for u in qs])
)
return inner
auth_backends.filter_users_by_email = decorate_for_auth(
auth_backends.filter_users_by_email
)
auth_backends.filter_users_by_username = decorate_for_auth(
auth_backends.filter_users_by_username
)
class ModelBackend(backends.ModelBackend):
@ -7,11 +36,17 @@ class ModelBackend(backends.ModelBackend):
Select related to avoid two additional queries
"""
try:
user = (
get_user_model()
._default_manager.select_related("actor__domain")
.get(pk=user_id)
)
user = get_user_model().objects.all().for_auth().get(pk=user_id)
except get_user_model().DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
def user_can_authenticate(self, user):
return super().user_can_authenticate(
user
) and not authentication.should_verify_email(user)
class AllAuthBackend(auth_backends.AuthenticationBackend, ModelBackend):
pass

View file

@ -31,6 +31,23 @@ class GroupFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
self.permissions.add(*perms)
@registry.register
class EmailAddressFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
verified = False
primary = True
class Meta:
model = "account.EmailAddress"
@registry.register
class EmailConfirmationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
email_address = factory.SubFactory(EmailAddressFactory)
class Meta:
model = "account.EmailConfirmation"
@registry.register
class InvitationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
owner = factory.LazyFunction(lambda: UserFactory())
@ -91,6 +108,16 @@ class UserFactory(factory.django.DjangoModelFactory):
self.save(update_fields=["actor"])
return self.actor
@factory.post_generation
def verified_email(self, create, extracted, **kwargs):
if not create or extracted is None:
return
return EmailConfirmationFactory(
email_address__verified=extracted,
email_address__email=self.email,
email_address__user=self,
)
@registry.register(name="users.SuperUser")
class SuperUserFactory(UserFactory):
@ -129,6 +156,7 @@ class AccessTokenFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
expires = factory.Faker("future_datetime", tzinfo=pytz.UTC)
token = factory.Faker("uuid4")
scope = "read"
class Meta:
model = "users.AccessToken"

View file

@ -9,13 +9,14 @@ import string
import uuid
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.db import models
from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from allauth.account.models import EmailAddress
from django_auth_ldap.backend import populate_user as ldap_populate_user
from oauth2_provider import models as oauth2_models
from oauth2_provider import validators as oauth2_validators
@ -94,6 +95,26 @@ def get_default_funkwhale_support_message_display_date():
)
class UserQuerySet(models.QuerySet):
def for_auth(self):
"""Optimization to avoid additional queries during authentication"""
qs = self.select_related("actor__domain")
verified_emails = EmailAddress.objects.filter(
user=models.OuterRef("id"), primary=True
).values("verified")[:1]
subquery = models.Subquery(verified_emails)
return qs.annotate(has_verified_primary_email=subquery)
class UserManager(BaseUserManager):
def get_queryset(self):
return UserQuerySet(self.model, using=self._db)
def get_by_natural_key(self, key):
obj = BaseUserManager.get_by_natural_key(self.all().for_auth(), key)
return obj
class User(AbstractUser):
# First Name and Last Name do not cover name patterns
@ -169,6 +190,8 @@ class User(AbstractUser):
blank=True,
)
objects = UserManager()
def __str__(self):
return self.username

View file

@ -1,11 +1,24 @@
import urllib.parse
import oauthlib.oauth2
from funkwhale_api.common import authentication
def check(request):
user = request.user
request.user = user.__class__.objects.all().for_auth().get(pk=user.pk)
if authentication.should_verify_email(request.user):
setattr(request, "oauth2_error", {"error": "unverified_email"})
return False
return True
class OAuth2Server(oauthlib.oauth2.Server):
def verify_request(self, uri, *args, **kwargs):
valid, request = super().verify_request(uri, *args, **kwargs)
if valid:
if not check(request):
return False, request
return valid, request
# maybe the token was given in the querystring?
@ -21,5 +34,8 @@ class OAuth2Server(oauthlib.oauth2.Server):
valid = self.request_validator.validate_bearer_token(
token, request.scopes, request
)
if valid:
if not check(request):
return False, request
return valid, request