From 237d6d312522975271cb4abf725be17aded7bcf3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 9 Aug 2019 11:51:34 +0300 Subject: [PATCH] version 0.0.5.3: added method to login user with username and password --- apps/authorization/serializers/common.py | 63 +++++++++++++++++- apps/authorization/serializers/web.py | 80 ----------------------- apps/authorization/urls/common.py | 4 +- apps/authorization/views/web.py | 83 +++++++++++++++++++----- 4 files changed, 128 insertions(+), 102 deletions(-) diff --git a/apps/authorization/serializers/common.py b/apps/authorization/serializers/common.py index d551fd07..11974bac 100644 --- a/apps/authorization/serializers/common.py +++ b/apps/authorization/serializers/common.py @@ -1,10 +1,67 @@ """Common serializer for application authorization""" +from django.contrib.auth import password_validation as password_validators from rest_framework import serializers +from rest_framework import validators as rest_validators + +from account import models as account_models from authorization.models import Application -class OAuth2Serialzier(serializers.Serializer): - """Serializer OAuth2 authorization""" - +# Mixins +class BaseAuthSerializerMixin(serializers.Serializer): + """Base authorization serializer mixin""" source = serializers.ChoiceField(choices=Application.SOURCES) + + +# Classic +class SignUpSerializer(BaseAuthSerializerMixin, serializers.ModelSerializer): + """Serializer for signing up user""" + email = serializers.CharField( + validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()), ), + write_only=True + ) + username = serializers.CharField( + validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()), ), + write_only=True + ) + password = serializers.CharField(write_only=True) + newsletter = serializers.BooleanField() + + class Meta: + """Meta-class""" + model = account_models.User + fields = ('email', 'username', 'newsletter', + 'password', 'source') + + def validate_password(self, data): + """Custom password validation""" + try: + password_validators.validate_password(password=data) + except serializers.ValidationError as e: + raise serializers.ValidationError(e) + else: + return data + + def create(self, validated_data): + """Override create method""" + obj = account_models.User.objects.make(**validated_data) + return obj + + +class LoginSerializer(BaseAuthSerializerMixin, serializers.ModelSerializer): + """Serializer for login user""" + username = serializers.CharField( + write_only=True + ) + password = serializers.CharField(write_only=True) + + class Meta: + """Meta-class""" + model = account_models.User + fields = ('username', 'password', 'source') + + +# OAuth +class OAuth2Serialzier(BaseAuthSerializerMixin): + """Serializer OAuth2 authorization""" token = serializers.CharField(max_length=255) diff --git a/apps/authorization/serializers/web.py b/apps/authorization/serializers/web.py index 5d04bb4c..f0e08069 100644 --- a/apps/authorization/serializers/web.py +++ b/apps/authorization/serializers/web.py @@ -1,81 +1 @@ """Serializers for application authorization""" -from rest_framework import serializers -from rest_framework.authentication import authenticate -from rest_framework import validators as rest_validators -from account import models as account_models -from django.contrib.auth import password_validation as password_validators -from django.utils.translation import gettext_lazy as _ - - -class AuthTokenClassicSerializer(serializers.Serializer): - username = serializers.CharField( - label=_('Username'), - required=False - ) - password = serializers.CharField( - label=_("Password"), - style={'input_type': 'password'}, - trim_whitespace=False - ) - email = serializers.EmailField( - label=_("Email"), - required=False - ) - - def validate(self, attrs): - username = attrs.get('username') - password = attrs.get('password') - email = attrs.get('email') - - if username and password: - user = authenticate(request=self.context.get('request'), - username=username, password=password) - elif email and password: - user = authenticate(request=self.context.get('request'), - email=email, password=password) - else: - msg = _('Must include "phone" and "password".') - raise serializers.ValidationError(msg, code='authorization') - if user: - # From Django 1.10 onwards the `authenticate` call simply - # returns `None` for is_active=False users. - # (Assuming the default `ModelBackend` authentication backend.) - if not user.is_active: - msg = _('User account is disabled.') - raise serializers.ValidationError(msg, code='authorization') - attrs['user'] = user - return attrs - - -class SignUpSerializer(serializers.ModelSerializer): - """Serializer for signing up user""" - email = serializers.CharField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()), ), - write_only=True - ) - username = serializers.CharField( - validators=(rest_validators.UniqueValidator(queryset=account_models.User.objects.all()), ), - write_only=True - ) - password = serializers.CharField(write_only=True) - newsletter = serializers.BooleanField() - - class Meta: - """Meta-class""" - model = account_models.User - fields = ('email', 'username', - 'newsletter', 'password') - - def validate_password(self, data): - """Custom password validation""" - try: - password_validators.validate_password(password=data) - except serializers.ValidationError as e: - raise serializers.ValidationError(e) - else: - return data - - def create(self, validated_data): - """Override create method""" - obj = account_models.User.objects.make(**validated_data) - return obj diff --git a/apps/authorization/urls/common.py b/apps/authorization/urls/common.py index 2426fcc6..80a9e4a5 100644 --- a/apps/authorization/urls/common.py +++ b/apps/authorization/urls/common.py @@ -30,13 +30,13 @@ urlpatterns_social_django = [ urlpatterns_rest_framework_social_oauth2 = [ url(r'^authorize/?$', AuthorizationView.as_view(), name="authorize"), - url(r'^token/?$', TokenView.as_view(), name="token"), url('', include('social_django.urls', namespace="social")), url(r'^invalidate-sessions/?$', invalidate_sessions, name="invalidate_sessions") ] urlpatterns_api = [ - path('social/signup/', views.SocialSignUpView.as_view(), name='signup'), + path('social/signup/', views.SocialSignUpView.as_view(), name='signup-social'), + path('login/', views.LoginView.as_view(), name='login-classic'), path('revoke-token/', views.RevokeTokenView.as_view(), name="revoke_token"), ] diff --git a/apps/authorization/views/web.py b/apps/authorization/views/web.py index f4ca1d71..9ea0d9d4 100644 --- a/apps/authorization/views/web.py +++ b/apps/authorization/views/web.py @@ -8,7 +8,7 @@ from oauth2_provider.oauth2_backends import OAuthLibCore from oauth2_provider.settings import oauth2_settings from oauth2_provider.views.mixins import OAuthLibMixin from rest_framework import permissions -from rest_framework.generics import GenericAPIView +from rest_framework.generics import GenericAPIView, CreateAPIView from rest_framework.response import Response from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer @@ -28,7 +28,8 @@ class OAuth2ViewMixin(GenericAPIView): if qs.exists(): return qs.first().client_id else: - raise utils_exceptions.SerivceError(data=_('Application is not found')) + raise utils_exceptions.SerivceError(data={ + 'detail': _('Application is not found')}) def get_client_secret(self, source) -> str: """Get application client id""" @@ -37,30 +38,70 @@ class OAuth2ViewMixin(GenericAPIView): if qs.exists: return qs.first().client_secret else: - raise utils_exceptions.SerivceError( - data=_('Not found an application with this source')) + raise utils_exceptions.SerivceError(data={ + 'detail': _('Not found an application with this source')}) - def prepare_request_data(self, request_data): + def prepare_request_data(self, validated_data: dict) -> dict: """Preparing request data""" - serializer = self.get_serializer(data=request_data) - serializer.is_valid(raise_exception=True) - # Set attributes - source = serializer.validated_data.get('source') - token = serializer.validated_data.get('token') + source = validated_data.get('source') # Set OAuth2 request parameters _request_data = { - 'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE, - 'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME, - 'token': token, 'client_id': self.get_client_id(source) } # Fill client secret parameter by platform - if source == Application.MOBILE: + if validated_data.get('source') == Application.MOBILE: _request_data['client_secret'] = self.get_client_secret(source) - return _request_data + # Fill token parameter if transfer + if validated_data.get('token'): + _request_data['token'] = validated_data.get('token') + if _request_data: + return _request_data + else: + raise utils_exceptions.SerivceError(data={ + 'detail': 'Unknown request data'}) # Create your views here. +class LoginView(CsrfExemptMixin, OAuthLibMixin, + OAuth2ViewMixin, GenericAPIView): + """ + Implements an endpoint to provide access tokens + + The endpoint is used in the following flows: + + * Authorization code + * Password + * Client credentials + """ + server_class = oauth2_settings.OAUTH2_SERVER_CLASS + validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS + oauthlib_backend_class = OAuthLibCore + permission_classes = (permissions.AllowAny,) + serializer_class = serializers.LoginSerializer + + def post(self, request, *args, **kwargs): + # Preparing request data + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + request_data = self.prepare_request_data(serializer.validated_data) + request_data.update({ + 'grant_type': 'password', + 'username': serializer.validated_data.get('username'), + 'password': serializer.validated_data.get('password'), + }) + # Use the rest framework `.data` to fake the post body of the django request. + request._request.POST = request._request.POST.copy() + for key, value in request_data.items(): + request._request.POST[key] = value + + url, headers, body, status = self.create_token_response(request._request) + response = Response(data=json.loads(body), status=status) + + for k, v in headers.items(): + response[k] = v + return response + + class SocialSignUpView(CsrfExemptMixin, OAuthLibMixin, OAuth2ViewMixin, GenericAPIView): """ @@ -80,7 +121,13 @@ class SocialSignUpView(CsrfExemptMixin, OAuthLibMixin, def post(self, request, *args, **kwargs): """Override POST method""" # Preparing request data - request_data = self.prepare_request_data(request_data=request.data) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + request_data = self.prepare_request_data(serializer.validated_data) + request_data.update({ + 'grant_type': settings.OAUTH2_SOCIAL_AUTH_GRANT_TYPE, + 'backend': settings.OAUTH2_SOCIAL_AUTH_BACKEND_NAME + }) # Use the rest framework `.data` to fake the post body of the django request. request._request.POST = request._request.POST.copy() for key, value in request_data.items(): @@ -108,7 +155,9 @@ class RevokeTokenView(CsrfExemptMixin, OAuthLibMixin, def post(self, request, *args, **kwargs): # Use the rest framework `.data` to fake the post body of the django request. # Preparing request data - request_data = self.prepare_request_data(request_data=request.data) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + request_data = self.prepare_request_data(serializer.validated_data) # Use the rest framework `.data` to fake the post body of the django request. request._request.POST = request._request.POST.copy() for key, value in request_data.items():