From b56f90900238baee96631cf309dcc2480fa77fa0 Mon Sep 17 00:00:00 2001 From: eugena Date: Tue, 29 Sep 2015 13:00:23 +0500 Subject: [PATCH 1/5] Ability to switch off new password confirmation on password change endpoint --- docs/configuration.rst | 3 +++ rest_auth/serializers.py | 11 +++++++++++ rest_auth/tests.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index ed0d785..dbe965b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -34,3 +34,6 @@ Configuration - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) + + +- **NEW_PASSWORD_2_FIELD_ENABLED** - set it to False if you don't need new password confirmation (default: True) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 2b671da..edbe58d 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -182,11 +182,18 @@ class PasswordChangeSerializer(serializers.Serializer): self.old_password_field_enabled = getattr( settings, 'OLD_PASSWORD_FIELD_ENABLED', False ) + + self.new_password_2_field_enabled = getattr( + settings, 'NEW_PASSWORD_2_FIELD_ENABLED', True + ) super(PasswordChangeSerializer, self).__init__(*args, **kwargs) if not self.old_password_field_enabled: self.fields.pop('old_password') + if not self.new_password_2_field_enabled: + self.fields.pop('new_password2') + self.request = self.context.get('request') self.user = getattr(self.request, 'user', None) @@ -202,6 +209,10 @@ class PasswordChangeSerializer(serializers.Serializer): return value def validate(self, attrs): + + if not self.new_password_2_field_enabled: + attrs['new_password2'] = attrs['new_password1'] + self.set_password_form = self.set_password_form_class( user=self.user, data=attrs ) diff --git a/rest_auth/tests.py b/rest_auth/tests.py index b137313..c084b91 100644 --- a/rest_auth/tests.py +++ b/rest_auth/tests.py @@ -282,6 +282,44 @@ class APITestCase1(TestCase, BaseAPITestCase): login_payload['password'] = new_password_payload['new_password1'] self.post(self.login_url, data=login_payload, status_code=200) + @override_settings(OLD_PASSWORD_FIELD_ENABLED=True, NEW_PASSWORD_2_FIELD_ENABLED=False) + def test_password_change_without_confirmation(self): + login_payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + self.post(self.login_url, data=login_payload, status_code=200) + self.token = self.response.json['key'] + + new_password_payload = { + "old_password": "%s!" % self.PASS, # wrong password + "new_password1": "new_person", + } + self.post( + self.password_change_url, + data=new_password_payload, + status_code=400 + ) + + new_password_payload = { + "old_password": self.PASS, + "new_password1": "new_person", + } + + self.post( + self.password_change_url, + data=new_password_payload, + status_code=200 + ) + + # user should not be able to login using old password + self.post(self.login_url, data=login_payload, status_code=400) + + # new password should work + login_payload['password'] = new_password_payload['new_password1'] + self.post(self.login_url, data=login_payload, status_code=200) + def test_password_reset(self): user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) From 04136c6e35fb0dc432c16de24026127a9840b645 Mon Sep 17 00:00:00 2001 From: eugena Date: Tue, 29 Sep 2015 16:12:48 +0500 Subject: [PATCH 2/5] Customization of including of user details urls --- docs/configuration.rst | 3 +++ rest_auth/urls.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index dbe965b..db7e2cd 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -37,3 +37,6 @@ Configuration - **NEW_PASSWORD_2_FIELD_ENABLED** - set it to False if you don't need new password confirmation (default: True) + + +- **USER_DETAILS_INCLUDED** - is user details urls are needed diff --git a/rest_auth/urls.py b/rest_auth/urls.py index d753c44..866515f 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,5 +1,7 @@ from django.conf.urls import patterns, url +from django.conf import settings + from rest_auth.views import ( LoginView, LogoutView, UserDetailsView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView @@ -15,7 +17,12 @@ urlpatterns = patterns( url(r'^login/$', LoginView.as_view(), name='rest_login'), # URLs that require a user to be logged in with a valid session / token. url(r'^logout/$', LogoutView.as_view(), name='rest_logout'), - url(r'^user/$', UserDetailsView.as_view(), name='rest_user_details'), url(r'^password/change/$', PasswordChangeView.as_view(), name='rest_password_change'), ) + +if getattr(settings, 'USER_DETAILS_INCLUDED', True): + urlpatterns += patterns( + '', + url(r'^user/$', UserDetailsView.as_view(), name='rest_user_details'), +) From 825b7c8def2a44270c221046907a03fcd081ed65 Mon Sep 17 00:00:00 2001 From: eugena Date: Tue, 29 Sep 2015 16:15:56 +0500 Subject: [PATCH 3/5] Ability to use simplified login --- docs/configuration.rst | 5 +++++ rest_auth/app_settings.py | 5 +++++ rest_auth/serializers.py | 28 ++++++++++++++++++++++++++++ rest_auth/urls.py | 14 ++++++++++++-- rest_auth/views.py | 34 ++++++++++++++++++++++++++++++++-- 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index db7e2cd..afdf4c6 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -8,6 +8,8 @@ Configuration - LOGIN_SERIALIZER - serializer class in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.LoginSerializer`` + - SIMPLE_LOGIN_SERIALIZER - serializer class in ``rest_auth.views.SimpleLoginView``, default value ``rest_auth.serializers.SimpleLoginSerializer`` + - TOKEN_SERIALIZER - response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.TokenSerializer`` - USER_DETAILS_SERIALIZER - serializer class in ``rest_auth.views.UserDetailsView``, default value ``rest_auth.serializers.UserDetailsSerializer`` @@ -40,3 +42,6 @@ Configuration - **USER_DETAILS_INCLUDED** - is user details urls are needed + + +- **SIMPLE_LOGIN** - is simplified is used \ No newline at end of file diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index e0340b7..da9832d 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -4,6 +4,7 @@ from rest_auth.serializers import ( TokenSerializer as DefaultTokenSerializer, UserDetailsSerializer as DefaultUserDetailsSerializer, LoginSerializer as DefaultLoginSerializer, + SimpleLoginSerializer as DefaultSimpleLoginSerializer, PasswordResetSerializer as DefaultPasswordResetSerializer, PasswordResetConfirmSerializer as DefaultPasswordResetConfirmSerializer, PasswordChangeSerializer as DefaultPasswordChangeSerializer) @@ -23,6 +24,10 @@ LoginSerializer = import_callable( serializers.get('LOGIN_SERIALIZER', DefaultLoginSerializer) ) +SimpleLoginSerializer = import_callable( + serializers.get('SIMPLE_LOGIN_SERIALIZER', DefaultSimpleLoginSerializer) +) + PasswordResetSerializer = import_callable( serializers.get( 'PASSWORD_RESET_SERIALIZER', diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index edbe58d..22783b0 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -14,6 +14,34 @@ from rest_framework.authtoken.models import Token from rest_framework.exceptions import ValidationError +class SimpleLoginSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField(style={'input_type': 'password'}) + + def validate(self, attrs): + username = attrs.get('username') + password = attrs.get('password') + + if username and password: + user = authenticate(username=username, password=password) + + else: + msg = _('Must include "username" and "password".') + raise exceptions.ValidationError(msg) + + # Did we get back an active user? + if user: + if not user.is_active: + msg = _('User account is disabled.') + raise exceptions.ValidationError(msg) + else: + msg = _('Unable to log in with provided credentials.') + raise exceptions.ValidationError(msg) + + attrs['user'] = user + return attrs + + class LoginSerializer(serializers.Serializer): username = serializers.CharField(required=False, allow_blank=True) email = serializers.EmailField(required=False, allow_blank=True) diff --git a/rest_auth/urls.py b/rest_auth/urls.py index 866515f..5da939d 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -3,7 +3,7 @@ from django.conf.urls import patterns, url from django.conf import settings from rest_auth.views import ( - LoginView, LogoutView, UserDetailsView, PasswordChangeView, + LoginView, SimpleLoginView, LogoutView, UserDetailsView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView ) @@ -14,7 +14,6 @@ urlpatterns = patterns( name='rest_password_reset'), url(r'^password/reset/confirm/$', PasswordResetConfirmView.as_view(), name='rest_password_reset_confirm'), - url(r'^login/$', LoginView.as_view(), name='rest_login'), # URLs that require a user to be logged in with a valid session / token. url(r'^logout/$', LogoutView.as_view(), name='rest_logout'), url(r'^password/change/$', PasswordChangeView.as_view(), @@ -26,3 +25,14 @@ if getattr(settings, 'USER_DETAILS_INCLUDED', True): '', url(r'^user/$', UserDetailsView.as_view(), name='rest_user_details'), ) + +if getattr(settings, 'SIMPLE_LOGIN', False): + urlpatterns += patterns( + '', + url(r'^login/$', SimpleLoginView.as_view(), name='rest_login'), +) +else: + urlpatterns += patterns( + '', + url(r'^login/$', LoginView.as_view(), name='rest_login'), +) \ No newline at end of file diff --git a/rest_auth/views.py b/rest_auth/views.py index d789ac4..0a0aa33 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -10,11 +10,41 @@ from rest_framework.authtoken.models import Token from rest_framework.generics import RetrieveUpdateAPIView from .app_settings import ( - TokenSerializer, UserDetailsSerializer, LoginSerializer, - PasswordResetSerializer, PasswordResetConfirmSerializer, + TokenSerializer, UserDetailsSerializer, SimpleLoginSerializer, + LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, PasswordChangeSerializer ) +class SimpleLoginView(GenericAPIView): + + """ + Check the credentials and authenticated if the credentials are valid . + Calls Django Auth login method to register User ID + in Django session framework + + Accept the following POST parameters: username, password + """ + permission_classes = (AllowAny,) + serializer_class = SimpleLoginSerializer + + def login(self): + self.user = self.serializer.validated_data['user'] + + if getattr(settings, 'REST_SESSION_LOGIN', True): + login(self.request, self.user) + + def get_error_response(self): + return Response( + self.serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + + def post(self, request, *args, **kwargs): + self.serializer = self.get_serializer(data=self.request.data) + if not self.serializer.is_valid(): + return self.get_error_response() + self.login() + return Response({}, status=status.HTTP_200_OK) + class LoginView(GenericAPIView): From 3cedbdc9bde7b404c470fb8bd1db6c3129b8855d Mon Sep 17 00:00:00 2001 From: eugena Date: Tue, 29 Sep 2015 16:17:03 +0500 Subject: [PATCH 4/5] Ability to use simplified login --- rest_auth/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_auth/urls.py b/rest_auth/urls.py index 5da939d..530e7d2 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -35,4 +35,4 @@ else: urlpatterns += patterns( '', url(r'^login/$', LoginView.as_view(), name='rest_login'), -) \ No newline at end of file +) From 3a15e58d53fc96e54736b3a5d0fb90498dad9cc9 Mon Sep 17 00:00:00 2001 From: eugena Date: Tue, 29 Sep 2015 17:49:23 +0500 Subject: [PATCH 5/5] launching change_password with changed settings in swagger --- rest_auth/serializers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 22783b0..c393f52 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -200,9 +200,9 @@ class PasswordResetConfirmSerializer(serializers.Serializer): class PasswordChangeSerializer(serializers.Serializer): - old_password = serializers.CharField(max_length=128) - new_password1 = serializers.CharField(max_length=128) - new_password2 = serializers.CharField(max_length=128) + old_password = serializers.CharField(max_length=128, required=False) + new_password1 = serializers.CharField(max_length=128, required=False) + new_password2 = serializers.CharField(max_length=128, required=False) set_password_form_class = SetPasswordForm @@ -225,6 +225,15 @@ class PasswordChangeSerializer(serializers.Serializer): self.request = self.context.get('request') self.user = getattr(self.request, 'user', None) + def get_fields(self): + if self.fields: + for field in self.fields: + self.fields[field].required = True + fields = self.fields + else: + fields = super(PasswordChangeSerializer, self).get_fields() + return fields + def validate_old_password(self, value): invalid_password_conditions = ( self.old_password_field_enabled,