From f0b96a8aa7acad27c4d179e744d4efb49608fd83 Mon Sep 17 00:00:00 2001 From: Gabriel Le Breton Date: Wed, 3 Jul 2019 11:07:31 -0400 Subject: [PATCH 1/4] Rename demo to requirements.txt and upgrade packages --- demo/requirements.pip | 6 ------ demo/requirements.txt | 6 ++++++ docs/demo.rst | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 demo/requirements.pip create mode 100644 demo/requirements.txt 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..93c8000 --- /dev/null +++ b/demo/requirements.txt @@ -0,0 +1,6 @@ +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 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 From 3e8a7e308cdef31d22af8fb30f3b7fbd413ae2ca Mon Sep 17 00:00:00 2001 From: Gabriel Le Breton Date: Wed, 3 Jul 2019 11:09:15 -0400 Subject: [PATCH 2/4] Add django-axes package with custom serializer --- demo/demo/settings.py | 34 +++++++++++++++++++++++++++---- demo/myapp/__init__.py | 0 demo/myapp/admin.py | 3 +++ demo/myapp/apps.py | 5 +++++ demo/myapp/migrations/__init__.py | 0 demo/myapp/models.py | 3 +++ demo/myapp/serializers.py | 16 +++++++++++++++ demo/myapp/tests.py | 3 +++ demo/myapp/views.py | 3 +++ demo/requirements.txt | 1 + 10 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 demo/myapp/__init__.py create mode 100644 demo/myapp/admin.py create mode 100644 demo/myapp/apps.py create mode 100644 demo/myapp/migrations/__init__.py create mode 100644 demo/myapp/models.py create mode 100644 demo/myapp/serializers.py create mode 100644 demo/myapp/tests.py create mode 100644 demo/myapp/views.py diff --git a/demo/demo/settings.py b/demo/demo/settings.py index 514a8fb..194b670 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.RestAuthLoginSerializer', + # 'TOKEN_SERIALIZER': 'path.to.custom.TokenSerializer', +} 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..1b9f590 --- /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 RestAuthLoginSerializer(LoginSerializer): + + def validate(self, attrs): + try: + attrs = super().validate(attrs) + except exceptions.ValidationError: + if getattr(self.context['request'], 'axes_locked_out', None): + raise serializers.ValidationError(get_lockout_message()) + return attrs diff --git a/demo/myapp/tests.py b/demo/myapp/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/demo/myapp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. 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.txt b/demo/requirements.txt index 93c8000..3eec24d 100644 --- a/demo/requirements.txt +++ b/demo/requirements.txt @@ -4,3 +4,4 @@ djangorestframework>=3.9.4 django-allauth>=0.39.1 six==1.12.0 django-rest-swagger==2.2.0 +django-axes==5.0.7 From 18983960a1fe2f35ebb7f752f71705b3314300e1 Mon Sep 17 00:00:00 2001 From: Gabriel Le Breton Date: Wed, 3 Jul 2019 11:09:27 -0400 Subject: [PATCH 3/4] Add docker setup for demo project --- demo/Dockerfile | 8 ++++++++ demo/demo/settings.py | 2 +- demo/docker-compose.yml | 13 +++++++++++++ demo/myapp/serializers.py | 8 ++++---- 4 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 demo/Dockerfile create mode 100644 demo/docker-compose.yml 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 194b670..ace0f2d 100644 --- a/demo/demo/settings.py +++ b/demo/demo/settings.py @@ -150,6 +150,6 @@ CACHES = { } REST_AUTH_SERIALIZERS = { - 'LOGIN_SERIALIZER': 'myapp.serializers.RestAuthLoginSerializer', + '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/serializers.py b/demo/myapp/serializers.py index 1b9f590..051f9c6 100644 --- a/demo/myapp/serializers.py +++ b/demo/myapp/serializers.py @@ -5,12 +5,12 @@ from rest_framework import exceptions # noinspection PyAbstractClass -class RestAuthLoginSerializer(LoginSerializer): +class RestAuthAxesLoginSerializer(LoginSerializer): def validate(self, attrs): try: - attrs = super().validate(attrs) - except exceptions.ValidationError: + return super().validate(attrs) + except exceptions.ValidationError as e: if getattr(self.context['request'], 'axes_locked_out', None): raise serializers.ValidationError(get_lockout_message()) - return attrs + raise e From b3d07028ca5ac4d798b2c0415eea044e7d7c9b2c Mon Sep 17 00:00:00 2001 From: Gabriel Le Breton Date: Wed, 3 Jul 2019 13:06:45 -0400 Subject: [PATCH 4/4] Add a few tests for the demo app and axes --- demo/myapp/serializers.py | 2 +- demo/myapp/tests.py | 3 -- demo/myapp/tests/test_serializers.py | 71 ++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) delete mode 100644 demo/myapp/tests.py create mode 100644 demo/myapp/tests/test_serializers.py diff --git a/demo/myapp/serializers.py b/demo/myapp/serializers.py index 051f9c6..cfd9d8b 100644 --- a/demo/myapp/serializers.py +++ b/demo/myapp/serializers.py @@ -7,7 +7,7 @@ from rest_framework import exceptions # noinspection PyAbstractClass class RestAuthAxesLoginSerializer(LoginSerializer): - def validate(self, attrs): + def validate(self, attrs) -> dict: try: return super().validate(attrs) except exceptions.ValidationError as e: diff --git a/demo/myapp/tests.py b/demo/myapp/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/demo/myapp/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. 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)