diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 0000000..ab2717b --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3 +ENV PYTHONUNBUFFERED 1 +RUN mkdir /code +WORKDIR /code +COPY requirements.txt /code/ +RUN pip install -r requirements.txt +COPY . /code/ + diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 514a8fb..ace0f2d 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -31,7 +31,7 @@ INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', - # 'django.contrib.messages', + 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', @@ -39,12 +39,15 @@ INSTALLED_APPS = ( 'rest_framework.authtoken', 'rest_auth', + 'allauth', 'allauth.account', 'rest_auth.registration', 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', 'rest_framework_swagger', + + 'axes', ) MIDDLEWARE = ( @@ -54,10 +57,9 @@ MIDDLEWARE = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) -# For backwards compatibility for Django 1.8 -MIDDLEWARE_CLASSES = MIDDLEWARE + 'axes.middleware.AxesMiddleware', +) ROOT_URLCONF = 'demo.urls' @@ -127,3 +129,27 @@ SWAGGER_SETTINGS = { 'LOGIN_URL': 'login', 'LOGOUT_URL': 'logout', } + +AUTHENTICATION_BACKENDS = [ + + # AxesBackend should be the first backend in the AUTHENTICATION_BACKENDS list. + 'axes.backends.AxesBackend', + + # Required for rest-auth when using Axes to prevent 'Unable to log in with provided credentials.' + 'allauth.account.auth_backends.AuthenticationBackend', + + # Django ModelBackend is the default authentication backend. + 'django.contrib.auth.backends.ModelBackend', +] + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', + 'LOCATION': 'django_database_cache', + } +} + +REST_AUTH_SERIALIZERS = { + 'LOGIN_SERIALIZER': 'myapp.serializers.RestAuthAxesLoginSerializer', + # 'TOKEN_SERIALIZER': 'path.to.custom.TokenSerializer', +} diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml new file mode 100644 index 0000000..321252c --- /dev/null +++ b/demo/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + web: + build: . + image: gableroux/django-rest-auth-demo + command: python manage.py runserver 0.0.0.0:1234 + environment: + PYTHONUNBUFFERED: 1 + volumes: + - .:/code + ports: + - "1234:1234" diff --git a/demo/myapp/__init__.py b/demo/myapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/myapp/admin.py b/demo/myapp/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/demo/myapp/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/demo/myapp/apps.py b/demo/myapp/apps.py new file mode 100644 index 0000000..74d6d13 --- /dev/null +++ b/demo/myapp/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MyappConfig(AppConfig): + name = 'myapp' diff --git a/demo/myapp/migrations/__init__.py b/demo/myapp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/myapp/models.py b/demo/myapp/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/demo/myapp/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/demo/myapp/serializers.py b/demo/myapp/serializers.py new file mode 100644 index 0000000..cfd9d8b --- /dev/null +++ b/demo/myapp/serializers.py @@ -0,0 +1,16 @@ +from axes.helpers import get_lockout_message +from rest_auth import serializers +from rest_auth.serializers import LoginSerializer +from rest_framework import exceptions + + +# noinspection PyAbstractClass +class RestAuthAxesLoginSerializer(LoginSerializer): + + def validate(self, attrs) -> dict: + try: + return super().validate(attrs) + except exceptions.ValidationError as e: + if getattr(self.context['request'], 'axes_locked_out', None): + raise serializers.ValidationError(get_lockout_message()) + raise e diff --git a/demo/myapp/tests/test_serializers.py b/demo/myapp/tests/test_serializers.py new file mode 100644 index 0000000..2402688 --- /dev/null +++ b/demo/myapp/tests/test_serializers.py @@ -0,0 +1,71 @@ +from django.conf import settings +from django.contrib.auth.models import User +from django.http import HttpRequest +from django.test import TestCase +from rest_framework.exceptions import ValidationError + +from myapp import serializers + + +class TestRestAuthAxesLoginSerializer(TestCase): + + def setUp(self) -> None: + self.request = HttpRequest() + + def test_validate_wrong_user(self) -> None: + serializer = serializers.RestAuthAxesLoginSerializer( + context=dict(request=self.request) + ) + with self.assertRaisesMessage(ValidationError, 'Unable to log in with provided credentials.'): + serializer.validate({ + 'username': 'test', + 'email': 'test@example.com', + 'password': 'test' + }) + + def test_validate_good_user(self) -> None: + User.objects.create_user( + username='test', + email='test@example.com', + password='test' + ) + serializer = serializers.RestAuthAxesLoginSerializer( + context=dict(request=self.request) + ) + attrs = serializer.validate({ + 'username': 'test', + 'email': 'test@example.com', + 'password': 'test' + }) + + self.assertIsNotNone(attrs) + + def test_validate_axes_locked_out(self) -> None: + good_password_creds = { + 'username': 'test', + 'email': 'test@example.com', + 'password': 'good_password' + } + + bad_password_creds = { + 'username': 'test', + 'email': 'test@example.com', + 'password': 'bad_password' + } + + User.objects.create_user(**good_password_creds) + serializer = serializers.RestAuthAxesLoginSerializer( + context=dict(request=self.request) + ) + + for i in range(settings.AXES_FAILURE_LIMIT - 1): + with self.assertRaisesMessage(ValidationError, 'Unable to log in with provided credentials.'): + serializer.validate(bad_password_creds) + + account_locked_message = 'Account locked: too many login attempts. Contact an admin to unlock your account.' + + with self.assertRaisesMessage(ValidationError, account_locked_message): + serializer.validate(bad_password_creds) + + with self.assertRaisesMessage(ValidationError, account_locked_message): + serializer.validate(good_password_creds) diff --git a/demo/myapp/views.py b/demo/myapp/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/demo/myapp/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/demo/requirements.pip b/demo/requirements.pip deleted file mode 100644 index 40b4820..0000000 --- a/demo/requirements.pip +++ /dev/null @@ -1,6 +0,0 @@ -django>=1.9.0 -django-rest-auth==0.9.5 -djangorestframework>=3.7.0 -django-allauth>=0.24.1 -six==1.9.0 -django-rest-swagger==2.0.7 diff --git a/demo/requirements.txt b/demo/requirements.txt new file mode 100644 index 0000000..3eec24d --- /dev/null +++ b/demo/requirements.txt @@ -0,0 +1,7 @@ +django==1.11.22 +django-rest-auth==0.9.5 +djangorestframework>=3.9.4 +django-allauth>=0.39.1 +six==1.12.0 +django-rest-swagger==2.2.0 +django-axes==5.0.7 diff --git a/docs/demo.rst b/docs/demo.rst index 877ca33..7fb6821 100644 --- a/docs/demo.rst +++ b/docs/demo.rst @@ -10,7 +10,7 @@ Do these steps to make it running (ideally in virtualenv). cd /tmp git clone https://github.com/Tivix/django-rest-auth.git cd django-rest-auth/demo/ - pip install -r requirements.pip + pip install -r requirements.txt python manage.py migrate --settings=demo.settings --noinput python manage.py runserver --settings=demo.settings