From b07bd83fa18f9e226e3c47c3dda3e8533f927c36 Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Wed, 1 Apr 2020 15:24:40 +0200 Subject: [PATCH] See #1039: resend confirmation email on login if email is unverified --- api/config/settings/common.py | 2 +- api/funkwhale_api/common/authentication.py | 44 ++++++++++++++++++++-- api/funkwhale_api/users/adapters.py | 4 ++ api/funkwhale_api/users/auth_backends.py | 8 ++-- api/funkwhale_api/users/jwt_views.py | 3 ++ api/funkwhale_api/users/oauth/server.py | 3 +- api/funkwhale_api/users/serializers.py | 11 ++++++ api/tests/users/oauth/test_views.py | 14 ++++++- deploy/env.prod.sample | 4 ++ 9 files changed, 82 insertions(+), 11 deletions(-) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index fe68a2746..ef52f6643 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -686,7 +686,7 @@ REST_FRAMEWORK = { "funkwhale_api.federation.parsers.ActivityParser", ), "DEFAULT_AUTHENTICATION_CLASSES": ( - "oauth2_provider.contrib.rest_framework.OAuth2Authentication", + "funkwhale_api.common.authentication.OAuth2Authentication", "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS", "funkwhale_api.common.authentication.BearerTokenHeaderAuth", "funkwhale_api.common.authentication.JSONWebTokenAuthentication", diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py index 6d2b433bc..f826b0c12 100644 --- a/api/funkwhale_api/common/authentication.py +++ b/api/funkwhale_api/common/authentication.py @@ -1,6 +1,13 @@ from django.conf import settings from django.utils.encoding import smart_text from django.utils.translation import ugettext as _ + +from django.core.cache import cache + +from allauth.account.utils import send_email_confirmation +from oauth2_provider.contrib.rest_framework.authentication import ( + OAuth2Authentication as BaseOAuth2Authentication, +) from rest_framework import exceptions from rest_framework_jwt import authentication from rest_framework_jwt.settings import api_settings @@ -14,7 +21,40 @@ def should_verify_email(user): return has_unverified_email and mandatory_verification +class UnverifiedEmail(Exception): + def __init__(self, user): + self.user = user + + +def resend_confirmation_email(request, user): + THROTTLE_DELAY = 500 + cache_key = "auth:resent-email-confirmation:{}".format(user.pk) + if cache.get(cache_key): + return False + + done = send_email_confirmation(request, user) + cache.set(cache_key, True, THROTTLE_DELAY) + return done + + +class OAuth2Authentication(BaseOAuth2Authentication): + def authenticate(self, request): + try: + return super().authenticate(request) + except UnverifiedEmail as e: + request.oauth2_error = {"error": "unverified_email"} + resend_confirmation_email(request, e.user) + + class BaseJsonWebTokenAuth(object): + def authenticate(self, request): + try: + return super().authenticate(request) + except UnverifiedEmail as e: + msg = _("You need to verify your email address.") + resend_confirmation_email(request, e.user) + raise exceptions.AuthenticationFailed(msg) + def authenticate_credentials(self, payload): """ We have to implement this method by hand to ensure we can check that the @@ -38,9 +78,7 @@ class BaseJsonWebTokenAuth(object): raise exceptions.AuthenticationFailed(msg) if should_verify_email(user): - - msg = _("You need to verify your email address.") - raise exceptions.AuthenticationFailed(msg) + raise UnverifiedEmail(user) return user diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py index c44a7ce9f..e52892bd9 100644 --- a/api/funkwhale_api/users/adapters.py +++ b/api/funkwhale_api/users/adapters.py @@ -25,3 +25,7 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter): def get_login_redirect_url(self, request): return "noop" + + def add_message(self, *args, **kwargs): + # disable message sending + return diff --git a/api/funkwhale_api/users/auth_backends.py b/api/funkwhale_api/users/auth_backends.py index b274bcee2..cb7d3deaf 100644 --- a/api/funkwhale_api/users/auth_backends.py +++ b/api/funkwhale_api/users/auth_backends.py @@ -43,9 +43,11 @@ class ModelBackend(backends.ModelBackend): 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) + can_authenticate = super().user_can_authenticate(user) + if authentication.should_verify_email(user): + raise authentication.UnverifiedEmail(user) + + return can_authenticate class AllAuthBackend(auth_backends.AuthenticationBackend, ModelBackend): diff --git a/api/funkwhale_api/users/jwt_views.py b/api/funkwhale_api/users/jwt_views.py index 532653abd..7d797a9b9 100644 --- a/api/funkwhale_api/users/jwt_views.py +++ b/api/funkwhale_api/users/jwt_views.py @@ -1,8 +1,11 @@ from rest_framework_jwt import views as jwt_views +from . import serializers + class ObtainJSONWebToken(jwt_views.ObtainJSONWebToken): throttling_scopes = {"*": {"anonymous": "jwt-login", "authenticated": "jwt-login"}} + serializer_class = serializers.JSONWebTokenSerializer class RefreshJSONWebToken(jwt_views.RefreshJSONWebToken): diff --git a/api/funkwhale_api/users/oauth/server.py b/api/funkwhale_api/users/oauth/server.py index e4f0947ec..cf2454420 100644 --- a/api/funkwhale_api/users/oauth/server.py +++ b/api/funkwhale_api/users/oauth/server.py @@ -8,8 +8,7 @@ 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 + raise authentication.UnverifiedEmail(user) return True diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 2027ee8c9..8952c5a19 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -7,8 +7,10 @@ from django.utils.translation import gettext_lazy as _ from rest_auth.serializers import PasswordResetSerializer as PRS from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter from rest_framework import serializers +from rest_framework_jwt import serializers as jwt_serializers from funkwhale_api.activity import serializers as activity_serializers +from funkwhale_api.common import authentication from funkwhale_api.common import models as common_models from funkwhale_api.common import preferences from funkwhale_api.common import serializers as common_serializers @@ -36,6 +38,15 @@ username_validators = [ASCIIUsernameValidator()] NOOP = object() +class JSONWebTokenSerializer(jwt_serializers.JSONWebTokenSerializer): + def validate(self, data): + try: + return super().validate(data) + except authentication.UnverifiedEmail as e: + authentication.send_email_confirmation(self.context["request"], e.user) + raise serializers.ValidationError("Please verify your email address.") + + class RegisterSerializer(RS): invitation = serializers.CharField( required=False, allow_null=True, allow_blank=True diff --git a/api/tests/users/oauth/test_views.py b/api/tests/users/oauth/test_views.py index 0bbbe0c57..bf78c83b4 100644 --- a/api/tests/users/oauth/test_views.py +++ b/api/tests/users/oauth/test_views.py @@ -381,9 +381,15 @@ def test_grant_delete(factories, logged_in_api_client, mocker, now): ], ) def test_token_auth( - setting_value, verified_email, expected_status_code, api_client, factories, settings + setting_value, + verified_email, + expected_status_code, + api_client, + factories, + settings, + mailoutbox, ): - + sent_emails = len(mailoutbox) user = factories["users.User"](verified_email=verified_email) token = factories["users.AccessToken"](user=user) settings.ACCOUNT_EMAIL_VERIFICATION = setting_value @@ -392,3 +398,7 @@ def test_token_auth( HTTP_AUTHORIZATION="Bearer {}".format(token.token), ) assert response.status_code == expected_status_code + + if expected_status_code != 200: + # confirmation email should have been sent again + assert len(mailoutbox) == sent_emails + 1 diff --git a/deploy/env.prod.sample b/deploy/env.prod.sample index fc88505e4..4a184e833 100644 --- a/deploy/env.prod.sample +++ b/deploy/env.prod.sample @@ -52,6 +52,10 @@ FUNKWHALE_PROTOCOL=https # EMAIL_CONFIG=smtp+ssl://user@:password@youremail.host:465 # EMAIL_CONFIG=smtp+tls://user@:password@youremail.host:587 +# Make email verification mandatory before using the service +# Doesn't apply to admins. +# ACCOUNT_EMAIL_VERIFICATION_ENFORCE=false + # The email address to use to send system emails. # DEFAULT_FROM_EMAIL=noreply@yourdomain