Skip to content

Commit

Permalink
Fix messy setting (Operation Clean Config Project) (#477)
Browse files Browse the repository at this point in the history
* Fix settings pt 1

* Fix messy settings pt 2

* fix messy setting pt 3

* Fix messy setting pt 4

* fix tests.utils.override_api_settings and tests.test_api.APIBasicTests.test_blacklisting

* fix tests.test_api.APIBasicTests.test_misconfigured_token_model

* fix tests.test_api.APIBasicTests.test_registration

* fix tests.test_api.APIBasicTests.test_registration_allowed_with_custom_no_password_serializer

* fix tests.test_social.TestSocialConnectAuth.test_social_connect

* fix tests.test_api.APIBasicTests.test_rotate_token_refresh_view

* delete utils.import_callable

* remove useless code in utils

* added new test tests.test_utils.TestFormatLazy.test_it_should_work

* fix tests.utils.override_api_settings

* added `'JWT_AUTH_HTTPONLY': False,` to demo project settings

* fix documentation

* remove `reload_api_settings`
  • Loading branch information
kiraware committed Feb 13, 2023
1 parent 59fccd7 commit cdbcf9c
Show file tree
Hide file tree
Showing 23 changed files with 595 additions and 353 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ urlpatterns = [
(Optional) Use Http-Only cookies

```python
REST_USE_JWT = True
JWT_AUTH_COOKIE = 'jwt-auth'
REST_AUTH = {
'USE_JWT': True,
'JWT_AUTH_COOKIE': 'jwt-auth',
}
```

### Testing
Expand Down
10 changes: 7 additions & 3 deletions demo/demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,19 @@
},
]

REST_SESSION_LOGIN = True
REST_AUTH = {
'SESSION_LOGIN': True,
'USE_JWT': True,
'JWT_AUTH_COOKIE': 'auth',
'JWT_AUTH_HTTPONLY': False,
}

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'username'
ACCOUNT_EMAIL_VERIFICATION = 'optional'

REST_USE_JWT = True
JWT_AUTH_COOKIE = 'auth'

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
Expand Down
110 changes: 69 additions & 41 deletions dj_rest_auth/app_settings.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,84 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework.settings import APISettings as _APISettings

from dj_rest_auth.serializers import JWTSerializer as DefaultJWTSerializer
from dj_rest_auth.serializers import (
JWTSerializerWithExpiration as DefaultJWTSerializerWithExpiration,
)
from dj_rest_auth.serializers import LoginSerializer as DefaultLoginSerializer
from dj_rest_auth.serializers import (
PasswordChangeSerializer as DefaultPasswordChangeSerializer,
)
from dj_rest_auth.serializers import (
PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer,
)
from dj_rest_auth.serializers import (
PasswordResetSerializer as DefaultPasswordResetSerializer,
)
from dj_rest_auth.serializers import TokenSerializer as DefaultTokenSerializer
from dj_rest_auth.serializers import (
UserDetailsSerializer as DefaultUserDetailsSerializer,
)

from .utils import default_create_token, import_callable
USER_SETTINGS = getattr(settings, "REST_AUTH", None)

DEFAULTS = {
'LOGIN_SERIALIZER': 'dj_rest_auth.serializers.LoginSerializer',
'TOKEN_SERIALIZER': 'dj_rest_auth.serializers.TokenSerializer',
'JWT_SERIALIZER': 'dj_rest_auth.serializers.JWTSerializer',
'JWT_SERIALIZER_WITH_EXPIRATION': 'dj_rest_auth.serializers.JWTSerializerWithExpiration',
'JWT_TOKEN_CLAIMS_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenObtainPairSerializer',
'USER_DETAILS_SERIALIZER': 'dj_rest_auth.serializers.UserDetailsSerializer',
'PASSWORD_RESET_SERIALIZER': 'dj_rest_auth.serializers.PasswordResetSerializer',
'PASSWORD_RESET_CONFIRM_SERIALIZER': 'dj_rest_auth.serializers.PasswordResetConfirmSerializer',
'PASSWORD_CHANGE_SERIALIZER': 'dj_rest_auth.serializers.PasswordChangeSerializer',

create_token = import_callable(getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token))
'REGISTER_SERIALIZER': 'dj_rest_auth.registration.serializers.RegisterSerializer',

serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {})
'REGISTER_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),

TokenSerializer = import_callable(serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer))
'TOKEN_MODEL': 'rest_framework.authtoken.models.Token',
'TOKEN_CREATOR': 'dj_rest_auth.utils.default_create_token',

JWTSerializer = import_callable(serializers.get('JWT_SERIALIZER', DefaultJWTSerializer))
'PASSWORD_RESET_USE_SITES_DOMAIN': False,
'OLD_PASSWORD_FIELD_ENABLED': False,
'LOGOUT_ON_PASSWORD_CHANGE': False,
'SESSION_LOGIN': True,
'USE_JWT': False,

JWTSerializerWithExpiration = import_callable(serializers.get('JWT_SERIALIZER_WITH_EXPIRATION', DefaultJWTSerializerWithExpiration))
'JWT_AUTH_COOKIE': None,
'JWT_AUTH_REFRESH_COOKIE': None,
'JWT_AUTH_REFRESH_COOKIE_PATH': '/',
'JWT_AUTH_SECURE': False,
'JWT_AUTH_HTTPONLY': True,
'JWT_AUTH_SAMESITE': 'Lax',
'JWT_AUTH_RETURN_EXPIRATION': False,
'JWT_AUTH_COOKIE_USE_CSRF': False,
'JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED': False,
}

UserDetailsSerializer = import_callable(serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer))
# List of settings that may be in string import notation.
IMPORT_STRINGS = (
'TOKEN_CREATOR',
'TOKEN_MODEL',
'TOKEN_SERIALIZER',
'JWT_SERIALIZER',
'JWT_SERIALIZER_WITH_EXPIRATION',
'JWT_TOKEN_CLAIMS_SERIALIZER',
'USER_DETAILS_SERIALIZER',
'LOGIN_SERIALIZER',
'PASSWORD_RESET_SERIALIZER',
'PASSWORD_RESET_CONFIRM_SERIALIZER',
'PASSWORD_CHANGE_SERIALIZER',
'REGISTER_SERIALIZER',
'REGISTER_PERMISSION_CLASSES',
)

LoginSerializer = import_callable(serializers.get('LOGIN_SERIALIZER', DefaultLoginSerializer))
# List of settings that have been removed
REMOVED_SETTINGS = ( )

PasswordResetSerializer = import_callable(
serializers.get(
'PASSWORD_RESET_SERIALIZER', DefaultPasswordResetSerializer,
),
)

PasswordResetConfirmSerializer = import_callable(
serializers.get(
'PASSWORD_RESET_CONFIRM_SERIALIZER', DefaultPasswordResetConfirmSerializer,
),
)
class APISettings(_APISettings): # pragma: no cover
def __check_user_settings(self, user_settings):
from .utils import format_lazy
SETTINGS_DOC = 'https://dj-rest-auth.readthedocs.io/en/latest/configuration.html'

for setting in REMOVED_SETTINGS:
if setting in user_settings:
raise RuntimeError(
format_lazy(
_(
"The '{}' setting has been removed. Please refer to '{}' for available settings."
),
setting,
SETTINGS_DOC,
)
)

return user_settings

PasswordChangeSerializer = import_callable(
serializers.get('PASSWORD_CHANGE_SERIALIZER', DefaultPasswordChangeSerializer),
)

JWT_AUTH_COOKIE = getattr(settings, 'JWT_AUTH_COOKIE', None)
JWT_AUTH_REFRESH_COOKIE = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE', None)
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
22 changes: 15 additions & 7 deletions dj_rest_auth/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
from django.contrib.sites.shortcuts import get_current_site
from django.urls import reverse

from .app_settings import api_settings


if 'allauth' in settings.INSTALLED_APPS:
from allauth.account import app_settings
from allauth.account import app_settings as allauth_account_settings
from allauth.account.adapter import get_adapter
from allauth.account.forms import \
ResetPasswordForm as DefaultPasswordResetForm
from allauth.account.forms import ResetPasswordForm as DefaultPasswordResetForm
from allauth.account.forms import default_token_generator
from allauth.account.utils import (filter_users_by_email,
user_pk_to_url_str, user_username)
from allauth.account.utils import (
filter_users_by_email,
user_pk_to_url_str,
user_username,
)
from allauth.utils import build_absolute_uri


Expand Down Expand Up @@ -44,7 +49,7 @@ def save(self, request, **kwargs):
args=[user_pk_to_url_str(user), temp_key],
)

if getattr(settings, 'REST_AUTH_PW_RESET_USE_SITES_DOMAIN', False) is True:
if api_settings.PASSWORD_RESET_USE_SITES_DOMAIN:
url = build_absolute_uri(None, path)
else:
url = build_absolute_uri(request, path)
Expand All @@ -57,7 +62,10 @@ def save(self, request, **kwargs):
'password_reset_url': url,
'request': request,
}
if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
if (
allauth_account_settings.AUTHENTICATION_METHOD
!= allauth_account_settings.AuthenticationMethod.EMAIL
):
context['username'] = user_username(user)
get_adapter(request).send_mail(
'account/email/password_reset_key', email, context
Expand Down
42 changes: 22 additions & 20 deletions dj_rest_auth/jwt_auth.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
from django.conf import settings
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework import exceptions, serializers
from rest_framework.authentication import CSRFCheck
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.serializers import TokenRefreshSerializer

from .app_settings import api_settings


def set_jwt_access_cookie(response, access_token):
from rest_framework_simplejwt.settings import api_settings as jwt_settings
cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None)
cookie_name = api_settings.JWT_AUTH_COOKIE
access_token_expiration = (timezone.now() + jwt_settings.ACCESS_TOKEN_LIFETIME)
cookie_secure = getattr(settings, 'JWT_AUTH_SECURE', False)
cookie_httponly = getattr(settings, 'JWT_AUTH_HTTPONLY', True)
cookie_samesite = getattr(settings, 'JWT_AUTH_SAMESITE', 'Lax')
cookie_secure = api_settings.JWT_AUTH_SECURE
cookie_httponly = api_settings.JWT_AUTH_HTTPONLY
cookie_samesite = api_settings.JWT_AUTH_SAMESITE

if cookie_name:
response.set_cookie(
Expand All @@ -29,11 +31,11 @@ def set_jwt_access_cookie(response, access_token):
def set_jwt_refresh_cookie(response, refresh_token):
from rest_framework_simplejwt.settings import api_settings as jwt_settings
refresh_token_expiration = (timezone.now() + jwt_settings.REFRESH_TOKEN_LIFETIME)
refresh_cookie_name = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE', None)
refresh_cookie_path = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE_PATH', '/')
cookie_secure = getattr(settings, 'JWT_AUTH_SECURE', False)
cookie_httponly = getattr(settings, 'JWT_AUTH_HTTPONLY', True)
cookie_samesite = getattr(settings, 'JWT_AUTH_SAMESITE', 'Lax')
refresh_cookie_name = api_settings.JWT_AUTH_REFRESH_COOKIE
refresh_cookie_path = api_settings.JWT_AUTH_REFRESH_COOKIE_PATH
cookie_secure = api_settings.JWT_AUTH_SECURE
cookie_httponly = api_settings.JWT_AUTH_HTTPONLY
cookie_samesite = api_settings.JWT_AUTH_SAMESITE

if refresh_cookie_name:
response.set_cookie(
Expand All @@ -53,10 +55,10 @@ def set_jwt_cookies(response, access_token, refresh_token):


def unset_jwt_cookies(response):
cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None)
refresh_cookie_name = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE', None)
refresh_cookie_path = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE_PATH', '/')
cookie_samesite = getattr(settings, 'JWT_AUTH_SAMESITE', 'Lax')
cookie_name = api_settings.JWT_AUTH_COOKIE
refresh_cookie_name = api_settings.JWT_AUTH_REFRESH_COOKIE
refresh_cookie_path = api_settings.JWT_AUTH_REFRESH_COOKIE_PATH
cookie_samesite = api_settings.JWT_AUTH_SAMESITE

if cookie_name:
response.delete_cookie(cookie_name, samesite=cookie_samesite)
Expand All @@ -71,7 +73,7 @@ def extract_refresh_token(self):
request = self.context['request']
if 'refresh' in request.data and request.data['refresh'] != '':
return request.data['refresh']
cookie_name = getattr(settings, 'JWT_AUTH_REFRESH_COOKIE', None)
cookie_name = api_settings.JWT_AUTH_REFRESH_COOKIE
if cookie_name and cookie_name in request.COOKIES:
return request.COOKIES.get(cookie_name)
else:
Expand All @@ -92,10 +94,10 @@ class RefreshViewWithCookieSupport(TokenRefreshView):
serializer_class = CookieTokenRefreshSerializer

def finalize_response(self, request, response, *args, **kwargs):
if response.status_code == 200 and 'access' in response.data:
if response.status_code == status.HTTP_200_OK and 'access' in response.data:
set_jwt_access_cookie(response, response.data['access'])
response.data['access_token_expiration'] = (timezone.now() + jwt_settings.ACCESS_TOKEN_LIFETIME)
if response.status_code == 200 and 'refresh' in response.data:
if response.status_code == status.HTTP_200_OK and 'refresh' in response.data:
set_jwt_refresh_cookie(response, response.data['refresh'])
return super().finalize_response(request, response, *args, **kwargs)
return RefreshViewWithCookieSupport
Expand All @@ -122,14 +124,14 @@ def dummy_get_response(request): # pragma: no cover
raise exceptions.PermissionDenied(f'CSRF Failed: {reason}')

def authenticate(self, request):
cookie_name = getattr(settings, 'JWT_AUTH_COOKIE', None)
cookie_name = api_settings.JWT_AUTH_COOKIE
header = self.get_header(request)
if header is None:
if cookie_name:
raw_token = request.COOKIES.get(cookie_name)
if getattr(settings, 'JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED', False): #True at your own risk
if api_settings.JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED: #True at your own risk
self.enforce_csrf(request)
elif raw_token is not None and getattr(settings, 'JWT_AUTH_COOKIE_USE_CSRF', False):
elif raw_token is not None and api_settings.JWT_AUTH_COOKIE_USE_CSRF:
self.enforce_csrf(request)
else:
return None
Expand Down
25 changes: 13 additions & 12 deletions dj_rest_auth/models.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
from rest_framework.authtoken.models import Token as DefaultTokenModel

from .app_settings import api_settings

def get_token_model():
default_model = 'rest_framework.authtoken.models.Token'
import_path = getattr(settings, 'REST_AUTH_TOKEN_MODEL', default_model)
session_login = getattr(settings, 'REST_SESSION_LOGIN', True)
use_jwt = getattr(settings, 'REST_USE_JWT', False)

if not any((session_login, import_path, use_jwt)):
token_model = api_settings.TOKEN_MODEL
session_login = api_settings.SESSION_LOGIN
use_jwt = api_settings.USE_JWT

if not any((session_login, token_model, use_jwt)):
raise ImproperlyConfigured(
'No authentication is configured for rest auth. You must enable one or '
'more of `REST_AUTH_TOKEN_MODEL`, `REST_USE_JWT` or `REST_SESSION_LOGIN`'
)
'more of `TOKEN_MODEL`, `USE_JWT` or `SESSION_LOGIN`'
)
if (
import_path == default_model
token_model == DefaultTokenModel
and 'rest_framework.authtoken' not in settings.INSTALLED_APPS
):
raise ImproperlyConfigured(
'You must include `rest_framework.authtoken` in INSTALLED_APPS '
'or set REST_AUTH_TOKEN_MODEL to None'
'or set TOKEN_MODEL to None'
)
return import_string(import_path) if import_path else None
return token_model

TokenModel = get_token_model()
20 changes: 0 additions & 20 deletions dj_rest_auth/registration/app_settings.py

This file was deleted.

0 comments on commit cdbcf9c

Please sign in to comment.