diff --git a/.env.dev b/.env.dev index e695ed785..4cad27e21 100644 --- a/.env.dev +++ b/.env.dev @@ -3,7 +3,7 @@ DJANGO_SETTINGS_MODULE=config.settings.local DJANGO_SECRET_KEY=dev C_FORCE_ROOT=true FUNKWHALE_HOSTNAME=localhost -FUNKWHALE_PROTOCOL=http +FUNKWHALE_PROTOCOL=https PYTHONDONTWRITEBYTECODE=true VUE_PORT=8080 MUSIC_DIRECTORY_PATH=/music diff --git a/api/config/api_urls.py b/api/config/api_urls.py index 04fbda87c..bbfadbd4e 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -10,7 +10,6 @@ from funkwhale_api.music import views from funkwhale_api.playlists import views as playlists_views from funkwhale_api.subsonic.views import SubsonicViewSet from funkwhale_api.tags import views as tags_views -from funkwhale_api.users import jwt_views router = common_routers.OptionalSlashRouter() router.register(r"activity", activity_views.ActivityViewSet, "activity") @@ -84,8 +83,6 @@ v1_patterns += [ r"^oauth/", include(("funkwhale_api.users.oauth.urls", "oauth"), namespace="oauth"), ), - url(r"^token/?$", jwt_views.obtain_jwt_token, name="token"), - url(r"^token/refresh/?$", jwt_views.refresh_jwt_token, name="token_refresh"), url(r"^rate-limit/?$", common_views.RateLimitView.as_view(), name="rate-limit"), url( r"^text-preview/?$", common_views.TextPreviewView.as_view(), name="text-preview" diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 2f207289c..9876db017 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals from collections import OrderedDict -import datetime import logging.config import sys @@ -812,13 +811,6 @@ def get_user_secret_key(user): return settings.SECRET_KEY + str(user.secret_key) -JWT_AUTH = { - "JWT_ALLOW_REFRESH": True, - "JWT_EXPIRATION_DELTA": datetime.timedelta(days=7), - "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30), - "JWT_AUTH_HEADER_PREFIX": "JWT", - "JWT_GET_USER_SECRET_KEY": get_user_secret_key, -} OLD_PASSWORD_FIELD_ENABLED = True AUTH_PASSWORD_VALIDATORS = [ { @@ -857,9 +849,6 @@ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "funkwhale_api.common.authentication.OAuth2Authentication", "funkwhale_api.common.authentication.ApplicationTokenAuthentication", - "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS", - "funkwhale_api.common.authentication.BearerTokenHeaderAuth", - "funkwhale_api.common.authentication.JSONWebTokenAuthentication", "rest_framework.authentication.BasicAuthentication", "rest_framework.authentication.SessionAuthentication", ), @@ -998,14 +987,6 @@ THROTTLING_RATES = { "rate": THROTTLING_USER_RATES.get("login", "30/hour"), "description": "Login", }, - "jwt-login": { - "rate": THROTTLING_USER_RATES.get("jwt-login", "30/hour"), - "description": "JWT token creation", - }, - "jwt-refresh": { - "rate": THROTTLING_USER_RATES.get("jwt-refresh", "30/hour"), - "description": "JWT token refresh", - }, "signup": { "rate": THROTTLING_USER_RATES.get("signup", "10/day"), "description": "Account creation", @@ -1052,7 +1033,6 @@ REST_AUTH_SERIALIZERS = { "PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer" # noqa } REST_SESSION_LOGIN = False -REST_USE_JWT = True ATOMIC_REQUESTS = False USE_X_FORWARDED_HOST = True diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py index 11447ce23..1340aed91 100644 --- a/api/funkwhale_api/common/authentication.py +++ b/api/funkwhale_api/common/authentication.py @@ -1,5 +1,4 @@ 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 @@ -9,8 +8,6 @@ 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 from funkwhale_api.users import models as users_models @@ -76,116 +73,3 @@ class ApplicationTokenAuthentication(object): request.scopes = application.scope.split() return user, None - - -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 - 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): - raise UnverifiedEmail(user) - - return user - - -class JSONWebTokenAuthenticationQS( - BaseJsonWebTokenAuth, authentication.BaseJSONWebTokenAuthentication -): - - www_authenticate_realm = "api" - - def get_jwt_value(self, request): - token = request.query_params.get("jwt") - if "jwt" in request.query_params and not token: - msg = _("Invalid Authorization header. No credentials provided.") - raise exceptions.AuthenticationFailed(msg) - return token - - def authenticate_header(self, request): - return '{0} realm="{1}"'.format( - api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm - ) - - -class BearerTokenHeaderAuth( - BaseJsonWebTokenAuth, authentication.BaseJSONWebTokenAuthentication -): - """ - For backward compatibility purpose, we used Authorization: JWT - but Authorization: Bearer is probably better. - """ - - www_authenticate_realm = "api" - - def get_jwt_value(self, request): - auth = authentication.get_authorization_header(request).split() - auth_header_prefix = "bearer" - - if not auth: - if api_settings.JWT_AUTH_COOKIE: - return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE) - return None - - if smart_text(auth[0].lower()) != auth_header_prefix: - return None - - if len(auth) == 1: - msg = _("Invalid Authorization header. No credentials provided.") - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _( - "Invalid Authorization header. Credentials string " - "should not contain spaces." - ) - raise exceptions.AuthenticationFailed(msg) - - return auth[1] - - def authenticate_header(self, request): - return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm) - - def authenticate(self, request): - auth = super().authenticate(request) - if auth: - if not auth[0].actor: - auth[0].create_actor() - return auth - - -class JSONWebTokenAuthentication( - BaseJsonWebTokenAuth, authentication.JSONWebTokenAuthentication -): - def authenticate(self, request): - auth = super().authenticate(request) - - if auth: - if not auth[0].actor: - auth[0].create_actor() - return auth diff --git a/api/funkwhale_api/users/jwt_views.py b/api/funkwhale_api/users/jwt_views.py deleted file mode 100644 index 7d797a9b9..000000000 --- a/api/funkwhale_api/users/jwt_views.py +++ /dev/null @@ -1,18 +0,0 @@ -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): - throttling_scopes = { - "*": {"anonymous": "jwt-refresh", "authenticated": "jwt-refresh"} - } - - -obtain_jwt_token = ObtainJSONWebToken.as_view() -refresh_jwt_token = RefreshJSONWebToken.as_view() diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 58c16ac1a..72bc5833f 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -10,10 +10,8 @@ from allauth.account import models as allauth_models 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 @@ -42,15 +40,6 @@ 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/requirements/base.txt b/api/requirements/base.txt index 309335869..9048ee198 100644 --- a/api/requirements/base.txt +++ b/api/requirements/base.txt @@ -25,7 +25,6 @@ celery~=4.4.0 django-cors-headers~=3.4.0 musicbrainzngs~=0.7.1 djangorestframework~=3.11.0 -djangorestframework-jwt~=1.11.0 arrow~=0.15.5 persisting-theory~=0.2.0 django-versatileimagefield~=2.0.0 diff --git a/api/tests/common/test_authentication.py b/api/tests/common/test_authentication.py index 5678abcf6..ccd39fc5a 100644 --- a/api/tests/common/test_authentication.py +++ b/api/tests/common/test_authentication.py @@ -1,8 +1,5 @@ import pytest -from rest_framework import exceptions -from rest_framework_jwt.settings import api_settings as jwt_settings - from funkwhale_api.common import authentication @@ -33,35 +30,6 @@ def test_should_verify_email( assert authentication.should_verify_email(user) is expected -@pytest.mark.parametrize( - "setting_value, verified_email, expected", - [ - ("mandatory", False, True), - ("optional", False, False), - ("mandatory", True, False), - ("optional", True, False), - ], -) -def test_json_webtoken_auth_verify_email_validity( - setting_value, verified_email, expected, factories, settings, mocker, api_request -): - settings.ACCOUNT_EMAIL_VERIFICATION = setting_value - user = factories["users.User"](verified_email=verified_email) - should_verify = mocker.spy(authentication, "should_verify_email") - payload = jwt_settings.JWT_PAYLOAD_HANDLER(user) - token = jwt_settings.JWT_ENCODE_HANDLER(payload) - request = api_request.get("/", HTTP_AUTHORIZATION="JWT {}".format(token)) - - auth = authentication.JSONWebTokenAuthentication() - if expected is False: - assert auth.authenticate(request)[0] == user - else: - with pytest.raises(exceptions.AuthenticationFailed, match=r".*verify.*"): - auth.authenticate(request) - - should_verify.assert_called_once_with(user) - - def test_app_token_authentication(factories, api_request): user = factories["users.User"]() app = factories["users.Application"](user=user, scope="read write") diff --git a/api/tests/common/test_routers.py b/api/tests/common/test_routers.py index 5d1710f09..fa3711318 100644 --- a/api/tests/common/test_routers.py +++ b/api/tests/common/test_routers.py @@ -22,8 +22,6 @@ from django import urls "/api/v1/manage/accounts", "/api/v1/oauth/apps", "/api/v1/moderation/content-filters", - "/api/v1/token", - "/api/v1/token/refresh", "/api/v1/instance/settings", "/api/v1/instance/nodeinfo/2.0", ], diff --git a/api/tests/test_auth.py b/api/tests/test_auth.py index 653110f09..9758a0f37 100644 --- a/api/tests/test_auth.py +++ b/api/tests/test_auth.py @@ -5,20 +5,6 @@ jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER -def test_can_authenticate_using_jwt_token_param_in_url(factories, preferences, client): - user = factories["users.User"]() - preferences["common__api_authentication_required"] = True - url = reverse("api:v1:tracks-list") - response = client.get(url) - - assert response.status_code == 401 - - payload = jwt_payload_handler(user) - token = jwt_encode_handler(payload) - response = client.get(url, data={"jwt": token}) - assert response.status_code == 200 - - def test_can_authenticate_using_oauth_token_param_in_url( factories, preferences, client, mocker ): diff --git a/api/tests/users/test_jwt.py b/api/tests/users/test_jwt.py deleted file mode 100644 index d0fe1a1fa..000000000 --- a/api/tests/users/test_jwt.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest -from jwt.exceptions import DecodeError -from rest_framework_jwt.settings import api_settings - - -def test_can_invalidate_token_when_changing_user_secret_key(factories): - user = factories["users.User"]() - u1 = user.secret_key - jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - payload = jwt_payload_handler(user) - payload = jwt_encode_handler(payload) - - # this should work - api_settings.JWT_DECODE_HANDLER(payload) - - # now we update the secret key - user.update_secret_key() - user.save() - assert user.secret_key != u1 - - # token should be invalid - with pytest.raises(DecodeError): - api_settings.JWT_DECODE_HANDLER(payload) - - -def test_can_invalidate_token_when_changing_settings_secret_key(factories, settings): - settings.SECRET_KEY = "test1" - user = factories["users.User"]() - jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - payload = jwt_payload_handler(user) - payload = jwt_encode_handler(payload) - - # this should work - api_settings.JWT_DECODE_HANDLER(payload) - - # now we update the secret key - settings.SECRET_KEY = "test2" - - # token should be invalid - with pytest.raises(DecodeError): - api_settings.JWT_DECODE_HANDLER(payload) diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 9b30c78bb..52b599187 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -134,42 +134,6 @@ def test_can_fetch_data_from_api(api_client, factories): ) -def test_can_get_token_via_api(api_client, factories): - user = factories["users.User"]() - url = reverse("api:v1:token") - payload = {"username": user.username, "password": "test"} - - response = api_client.post(url, payload) - assert response.status_code == 200 - assert "token" in response.data - - -def test_can_get_token_via_api_inactive(api_client, factories): - user = factories["users.User"](is_active=False) - url = reverse("api:v1:token") - payload = {"username": user.username, "password": "test"} - - response = api_client.post(url, payload) - assert response.status_code == 400 - - -def test_can_refresh_token_via_api(api_client, factories, mocker): - # first, we get a token - user = factories["users.User"]() - url = reverse("api:v1:token") - payload = {"username": user.username, "password": "test"} - - response = api_client.post(url, payload) - assert response.status_code == 200 - - token = response.data["token"] - url = reverse("api:v1:token_refresh") - response = api_client.post(url, {"token": token}) - - assert response.status_code == 200 - assert "token" in response.data - - def test_changing_password_updates_secret_key(logged_in_api_client): user = logged_in_api_client.user password = user.password @@ -488,40 +452,6 @@ def test_signup_with_approval_enabled_validation_error( assert response.status_code == 400 -def test_user_login_jwt(factories, api_client): - user = factories["users.User"]() - data = { - "username": user.username, - "password": "test", - } - url = reverse("api:v1:token") - response = api_client.post(url, data) - assert response.status_code == 200 - - -@pytest.mark.parametrize( - "setting_value, verified_email, expected_status_code", - [ - ("mandatory", False, 400), - ("mandatory", True, 200), - ("optional", False, 200), - ("optional", True, 200), - ], -) -def test_user_login_jwt_honor_email_verification( - setting_value, verified_email, expected_status_code, settings, factories, api_client -): - settings.ACCOUNT_EMAIL_VERIFICATION = setting_value - user = factories["users.User"](verified_email=verified_email) - data = { - "username": user.username, - "password": "test", - } - url = reverse("api:v1:token") - response = api_client.post(url, data) - assert response.status_code == expected_status_code - - def test_login_via_api(api_client, factories): user = factories["users.User"]() url = reverse("api:v1:users:login") diff --git a/changes/changelog.d/1108.enhancement b/changes/changelog.d/1108.enhancement new file mode 100644 index 000000000..96e7b9e6d --- /dev/null +++ b/changes/changelog.d/1108.enhancement @@ -0,0 +1 @@ +Remove deprecated JWT Authentication (#1108) (1108)