Verified Commit b07bd83f authored by Eliot Berriot's avatar Eliot Berriot
Browse files

See #1039: resend confirmation email on login if email is unverified

parent 372bd4a6
...@@ -686,7 +686,7 @@ REST_FRAMEWORK = { ...@@ -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",
......
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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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):
......
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): ...@@ -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 _ ...@@ -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()] ...@@ -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): ...@@ -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( ...@@ -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 ...@@ -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
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment