See #1039: resend confirmation email on login if email is unverified
This commit is contained in:
		
							parent
							
								
									372bd4a6ee
								
							
						
					
					
						commit
						b07bd83fa1
					
				| 
						 | 
				
			
			@ -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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue