diff --git a/docs/configuration.rst b/docs/configuration.rst index ed0d785..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`` @@ -34,3 +36,12 @@ 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) + + +- **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 2b671da..c393f52 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) @@ -172,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 @@ -182,14 +210,30 @@ 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) + 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, @@ -202,6 +246,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) diff --git a/rest_auth/urls.py b/rest_auth/urls.py index d753c44..530e7d2 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,7 +1,9 @@ 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 ) @@ -12,10 +14,25 @@ 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'^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'), +) + +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'), +) 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):