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",
|
"funkwhale_api.federation.parsers.ActivityParser",
|
||||||
),
|
),
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
|
"funkwhale_api.common.authentication.OAuth2Authentication",
|
||||||
"funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS",
|
"funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS",
|
||||||
"funkwhale_api.common.authentication.BearerTokenHeaderAuth",
|
"funkwhale_api.common.authentication.BearerTokenHeaderAuth",
|
||||||
"funkwhale_api.common.authentication.JSONWebTokenAuthentication",
|
"funkwhale_api.common.authentication.JSONWebTokenAuthentication",
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
from django.utils.translation import ugettext as _
|
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 import exceptions
|
||||||
from rest_framework_jwt import authentication
|
from rest_framework_jwt import authentication
|
||||||
from rest_framework_jwt.settings import api_settings
|
from rest_framework_jwt.settings import api_settings
|
||||||
|
@ -14,7 +21,40 @@ def should_verify_email(user):
|
||||||
return has_unverified_email and mandatory_verification
|
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):
|
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):
|
def authenticate_credentials(self, payload):
|
||||||
"""
|
"""
|
||||||
We have to implement this method by hand to ensure we can check that the
|
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)
|
raise exceptions.AuthenticationFailed(msg)
|
||||||
|
|
||||||
if should_verify_email(user):
|
if should_verify_email(user):
|
||||||
|
raise UnverifiedEmail(user)
|
||||||
msg = _("You need to verify your email address.")
|
|
||||||
raise exceptions.AuthenticationFailed(msg)
|
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
|
@ -25,3 +25,7 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter):
|
||||||
|
|
||||||
def get_login_redirect_url(self, request):
|
def get_login_redirect_url(self, request):
|
||||||
return "noop"
|
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
|
return user if self.user_can_authenticate(user) else None
|
||||||
|
|
||||||
def user_can_authenticate(self, user):
|
def user_can_authenticate(self, user):
|
||||||
return super().user_can_authenticate(
|
can_authenticate = super().user_can_authenticate(user)
|
||||||
user
|
if authentication.should_verify_email(user):
|
||||||
) and not authentication.should_verify_email(user)
|
raise authentication.UnverifiedEmail(user)
|
||||||
|
|
||||||
|
return can_authenticate
|
||||||
|
|
||||||
|
|
||||||
class AllAuthBackend(auth_backends.AuthenticationBackend, ModelBackend):
|
class AllAuthBackend(auth_backends.AuthenticationBackend, ModelBackend):
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from rest_framework_jwt import views as jwt_views
|
from rest_framework_jwt import views as jwt_views
|
||||||
|
|
||||||
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class ObtainJSONWebToken(jwt_views.ObtainJSONWebToken):
|
class ObtainJSONWebToken(jwt_views.ObtainJSONWebToken):
|
||||||
throttling_scopes = {"*": {"anonymous": "jwt-login", "authenticated": "jwt-login"}}
|
throttling_scopes = {"*": {"anonymous": "jwt-login", "authenticated": "jwt-login"}}
|
||||||
|
serializer_class = serializers.JSONWebTokenSerializer
|
||||||
|
|
||||||
|
|
||||||
class RefreshJSONWebToken(jwt_views.RefreshJSONWebToken):
|
class RefreshJSONWebToken(jwt_views.RefreshJSONWebToken):
|
||||||
|
|
|
@ -8,8 +8,7 @@ def check(request):
|
||||||
user = request.user
|
user = request.user
|
||||||
request.user = user.__class__.objects.all().for_auth().get(pk=user.pk)
|
request.user = user.__class__.objects.all().for_auth().get(pk=user.pk)
|
||||||
if authentication.should_verify_email(request.user):
|
if authentication.should_verify_email(request.user):
|
||||||
setattr(request, "oauth2_error", {"error": "unverified_email"})
|
raise authentication.UnverifiedEmail(user)
|
||||||
return False
|
|
||||||
return True
|
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.serializers import PasswordResetSerializer as PRS
|
||||||
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
from rest_auth.registration.serializers import RegisterSerializer as RS, get_adapter
|
||||||
from rest_framework import serializers
|
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.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 models as common_models
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.common import serializers as common_serializers
|
from funkwhale_api.common import serializers as common_serializers
|
||||||
|
@ -36,6 +38,15 @@ username_validators = [ASCIIUsernameValidator()]
|
||||||
NOOP = object()
|
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):
|
class RegisterSerializer(RS):
|
||||||
invitation = serializers.CharField(
|
invitation = serializers.CharField(
|
||||||
required=False, allow_null=True, allow_blank=True
|
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(
|
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)
|
user = factories["users.User"](verified_email=verified_email)
|
||||||
token = factories["users.AccessToken"](user=user)
|
token = factories["users.AccessToken"](user=user)
|
||||||
settings.ACCOUNT_EMAIL_VERIFICATION = setting_value
|
settings.ACCOUNT_EMAIL_VERIFICATION = setting_value
|
||||||
|
@ -392,3 +398,7 @@ def test_token_auth(
|
||||||
HTTP_AUTHORIZATION="Bearer {}".format(token.token),
|
HTTP_AUTHORIZATION="Bearer {}".format(token.token),
|
||||||
)
|
)
|
||||||
assert response.status_code == expected_status_code
|
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+ssl://user@:password@youremail.host:465
|
||||||
# EMAIL_CONFIG=smtp+tls://user@:password@youremail.host:587
|
# 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.
|
# The email address to use to send system emails.
|
||||||
# DEFAULT_FROM_EMAIL=noreply@yourdomain
|
# DEFAULT_FROM_EMAIL=noreply@yourdomain
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue