diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 05e8691..1b55bb3 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -73,3 +73,4 @@ Basing on example from installation section :doc:`Installation ` - /rest-auth/facebook/ (POST) - access_token + - code diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index e3c69ff..dd49611 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -6,10 +6,11 @@ from allauth.socialaccount.helpers import complete_social_login class SocialLoginSerializer(serializers.Serializer): - access_token = serializers.CharField(required=True) + access_token = serializers.CharField(required=False) + code = serializers.CharField(required=False) def validate(self, attrs): - access_token = attrs.get('access_token') + view = self.context.get('view') request = self.context.get('request') if not isinstance(request, HttpRequest): @@ -19,20 +20,71 @@ class SocialLoginSerializer(serializers.Serializer): raise serializers.ValidationError( 'View is not defined, pass it as a context variable' ) - self.adapter_class = getattr(view, 'adapter_class', None) if not self.adapter_class: - raise serializers.ValidationError('Define adapter_class in view') + raise serializers.ValidationError( + 'Define adapter_class in view' + ) self.adapter = self.adapter_class() app = self.adapter.get_provider().get_app(request) + + # More info on code vs access_token + # http://stackoverflow.com/questions/8666316/facebook-oauth-2-0-code-and-token + # We have the access_token straight + if('access_token' in attrs): + access_token = attrs.get('access_token') + # We did not get the access_token, but authorization code instead + elif('code' in attrs): + self.callback_url = getattr(view, 'callback_url', None) + self.client_class = getattr(view, 'client_class', None) + + if not self.callback_url: + raise serializers.ValidationError( + 'Define callback_url in view' + ) + if not self.client_class: + raise serializers.ValidationError( + 'Define client_class in view' + ) + + if not self.callback_url: + raise serializers.ValidationError( + 'Define callback_url in view' + ) + + if not self.client_class: + raise serializers.ValidationError( + 'Define client_class in view' + ) + + code = attrs.get('code') + + provider = self.adapter.get_provider() + scope = provider.get_scope(request) + client = self.client_class( + request, + app.client_id, + app.secret, + self.adapter.access_token_method, + self.adapter.access_token_url, + self.callback_url, + scope + ) + token = client.get_access_token(code) + access_token = token['access_token'] + token = self.adapter.parse_token({'access_token': access_token}) token.app = app try: - login = self.adapter.complete_login(request, app, token, - response=access_token) + login = self.adapter.complete_login( + request, + app, + token, + response=access_token, + ) login.token = token complete_social_login(request, login) diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index 485881c..3dada33 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -2,20 +2,41 @@ from django.http import HttpRequest from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny +from rest_framework.authtoken.models import Token from rest_framework import status from allauth.account.views import SignupView, ConfirmEmailView from allauth.account.utils import complete_signup from allauth.account import app_settings -from rest_auth.app_settings import UserDetailsSerializer +from rest_auth.app_settings import ( + UserDetailsSerializer, + TokenSerializer, +) from rest_auth.registration.serializers import SocialLoginSerializer -from rest_auth.views import Login +from rest_auth.views import ( + Login, + EverybodyCanAuthentication, +) class Register(APIView, SignupView): + """ + Accepts the credentials and creates a new user + if user does not exist already + Return the REST Token and the user object + if the credentials are valid and authenticated. + Calls allauth complete_signup method + + Accept the following POST parameters: username, password + Return the REST Framework Token Object's key + and user object. + """ permission_classes = (AllowAny,) + authentication_classes = (EverybodyCanAuthentication,) + token_model = Token + token_serializer = TokenSerializer user_serializer_class = UserDetailsSerializer allowed_methods = ('POST', 'OPTIONS', 'HEAD') @@ -27,6 +48,8 @@ class Register(APIView, SignupView): def form_valid(self, form): self.user = form.save(self.request) + self.token, created = self.token_model.objects.get_or_create( + user=self.user) if isinstance(self.request, HttpRequest): request = self.request else: @@ -47,8 +70,10 @@ class Register(APIView, SignupView): return self.get_response_with_errors() def get_response(self): - serializer = self.user_serializer_class(instance=self.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) + response = self.token_serializer(self.token).data + user = self.user_serializer_class(instance=self.user).data + response['user'] = user + return Response(response, status=status.HTTP_201_CREATED) def get_response_with_errors(self): return Response(self.form.errors, status=status.HTTP_400_BAD_REQUEST) @@ -72,11 +97,25 @@ class VerifyEmail(APIView, ConfirmEmailView): class SocialLogin(Login): """ class used for social authentications - example usage for facebook + example usage for facebook with access_token + ------------- from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + class FacebookLogin(SocialLogin): adapter_class = FacebookOAuth2Adapter + + ------------- + example usage for facebook with code + + from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter + from allauth.socialaccount.providers.oauth2.client import OAuth2Client + + class FacebookLogin(SocialLogin): + adapter_class = FacebookOAuth2Adapter + client_class = OAuth2Client + callback_url = 'localhost:8000' + ------------- """ serializer_class = SocialLoginSerializer diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 8a11084..ac23a71 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -1,5 +1,7 @@ from django.contrib.auth import get_user_model +from django.contrib.auth import authenticate from django.conf import settings +from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm try: from django.utils.http import urlsafe_base64_decode as uid_decoder @@ -8,16 +10,76 @@ except: from django.utils.http import base36_to_int as uid_decoder from django.contrib.auth.tokens import default_token_generator -from rest_framework import serializers +from rest_framework import ( + exceptions, + serializers, +) from rest_framework.authtoken.models import Token -from rest_framework.authtoken.serializers import AuthTokenSerializer +# from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.exceptions import ValidationError -class LoginSerializer(AuthTokenSerializer): +class LoginSerializer(serializers.Serializer): + username = serializers.CharField(required=False) + email = serializers.EmailField(required=False) + password = serializers.CharField(style={'input_type': 'password'}) def validate(self, attrs): - attrs = super(LoginSerializer, self).validate(attrs) + username = attrs.get('username') + email = attrs.get('email') + password = attrs.get('password') + + if 'allauth' in settings.INSTALLED_APPS: + from allauth.account import app_settings + + # Authentication through email + if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL: + if email and password: + user = authenticate(email=email, password=password) + else: + msg = _('Must include "email" and "password".') + raise exceptions.ValidationError(msg) + # Authentication through username + elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: + if username and password: + user = authenticate(username=username, password=password) + else: + msg = _('Must include "username" and "password".') + raise exceptions.ValidationError(msg) + # Authentication through either username or email + else: + if email and password: + user = authenticate(email=email, password=password) + elif username and password: + user = authenticate(username=username, password=password) + else: + msg = _('Must include either "username" or "email" and "password".') + raise exceptions.ValidationError(msg) + + 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) + + elif username and password: + user = authenticate(username=username, password=password) + + 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) + else: + msg = _('Must include "username" and "password".') + raise exceptions.ValidationError(msg) + + attrs['user'] = user + if 'rest_auth.registration' in settings.INSTALLED_APPS: from allauth.account import app_settings if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: @@ -25,9 +87,24 @@ class LoginSerializer(AuthTokenSerializer): email_address = user.emailaddress_set.get(email=user.email) if not email_address.verified: raise serializers.ValidationError('E-mail is not verified.') + return attrs +# class LoginSerializer(AuthTokenSerializer): + +# def validate(self, attrs): +# attrs = super(LoginSerializer, self).validate(attrs) +# if 'rest_auth.registration' in settings.INSTALLED_APPS: +# from allauth.account import app_settings +# if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: +# user = attrs['user'] +# email_address = user.emailaddress_set.get(email=user.email) +# if not email_address.verified: +# raise serializers.ValidationError('E-mail is not verified.') +# return attrs + + class TokenSerializer(serializers.ModelSerializer): """ Serializer for Token model. diff --git a/rest_auth/views.py b/rest_auth/views.py index ab06528..31cccd4 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -8,29 +8,44 @@ from rest_framework.generics import GenericAPIView from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.authtoken.models import Token from rest_framework.generics import RetrieveUpdateAPIView +from rest_framework.authentication import SessionAuthentication from .app_settings import ( - TokenSerializer, UserDetailsSerializer, LoginSerializer, - PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer + TokenSerializer, + UserDetailsSerializer, + LoginSerializer, + PasswordResetSerializer, + PasswordResetConfirmSerializer, + PasswordChangeSerializer, ) +# http://bytefilia.com/titanium-mobile-facebook-application-django-allauth-sign-sign/ +class EverybodyCanAuthentication(SessionAuthentication): + def authenticate(self, request): + return None + + class Login(GenericAPIView): """ Check the credentials and return the REST Token + and the user object if the credentials are valid and authenticated. Calls Django Auth login method to register User ID in Django session framework Accept the following POST parameters: username, password - Return the REST Framework Token Object's key. + Return the REST Framework Token Object's key + and user object. """ + permission_classes = (AllowAny,) + authentication_classes = (EverybodyCanAuthentication,) serializer_class = LoginSerializer token_model = Token response_serializer = TokenSerializer + user_serializer = UserDetailsSerializer def login(self): self.user = self.serializer.validated_data['user'] @@ -40,13 +55,18 @@ class Login(GenericAPIView): login(self.request, self.user) def get_response(self): + response = self.response_serializer(self.token).data + user = self.user_serializer(instance=self.user).data + response['user'] = user return Response( - self.response_serializer(self.token).data, status=status.HTTP_200_OK + response, + status=status.HTTP_200_OK ) def get_error_response(self): return Response( - self.serializer.errors, status=status.HTTP_400_BAD_REQUEST + self.serializer.errors, + status=status.HTTP_400_BAD_REQUEST ) def post(self, request, *args, **kwargs): @@ -75,8 +95,10 @@ class Logout(APIView): logout(request) - return Response({"success": "Successfully logged out."}, - status=status.HTTP_200_OK) + return Response( + {"success": "Successfully logged out."}, + status=status.HTTP_200_OK + ) class UserDetails(RetrieveUpdateAPIView): @@ -127,7 +149,8 @@ class PasswordReset(GenericAPIView): class PasswordResetConfirm(GenericAPIView): """ - Password reset e-mail link is confirmed, therefore this resets the user's password. + Password reset e-mail link is confirmed, + therefore this resets the user's password. Accepts the following POST parameters: new_password1, new_password2 Accepts the following Django URL arguments: token, uid @@ -141,10 +164,13 @@ class PasswordResetConfirm(GenericAPIView): serializer = self.get_serializer(data=request.DATA) if not serializer.is_valid(): return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST + serializer.errors, + status=status.HTTP_400_BAD_REQUEST ) serializer.save() - return Response({"success": "Password has been reset with the new password."}) + return Response( + {"success": "Password has been reset with the new password."} + ) class PasswordChange(GenericAPIView): @@ -163,7 +189,10 @@ class PasswordChange(GenericAPIView): serializer = self.get_serializer(data=request.DATA) if not serializer.is_valid(): return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST + serializer.errors, + status=status.HTTP_400_BAD_REQUEST ) serializer.save() - return Response({"success": "New password has been saved."}) + return Response( + {"success": "New password has been saved."} + )