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/docs/configuration.rst b/docs/configuration.rst index f04d86d..ed0d785 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -6,17 +6,17 @@ Configuration You can define your custom serializers for each endpoint without overriding urls and views by adding ``REST_AUTH_SERIALIZERS`` dictionary in your django settings. Possible key values: - - LOGIN_SERIALIZER - serializer class in ``rest_auth.views.Login``, default value ``rest_auth.serializers.LoginSerializer`` + - LOGIN_SERIALIZER - serializer class in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.LoginSerializer`` - - TOKEN_SERIALIZER - response for successful authentication in ``rest_auth.views.Login``, default value ``rest_auth.serializers.TokenSerializer`` + - 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.UserDetails``, default value ``rest_auth.serializers.UserDetailsSerializer`` + - USER_DETAILS_SERIALIZER - serializer class in ``rest_auth.views.UserDetailsView``, default value ``rest_auth.serializers.UserDetailsSerializer`` - - PASSWORD_RESET_SERIALIZER - serializer class in ``rest_auth.views.PasswordReset``, default value ``rest_auth.serializers.PasswordResetSerializer`` + - PASSWORD_RESET_SERIALIZER - serializer class in ``rest_auth.views.PasswordResetView``, default value ``rest_auth.serializers.PasswordResetSerializer`` - - PASSWORD_RESET_CONFIRM_SERIALIZER - serializer class in ``rest_auth.views.PasswordResetConfirm``, default value ``rest_auth.serializers.PasswordResetConfirmSerializer`` + - PASSWORD_RESET_CONFIRM_SERIALIZER - serializer class in ``rest_auth.views.PasswordResetConfirmView``, default value ``rest_auth.serializers.PasswordResetConfirmSerializer`` - - PASSWORD_CHANGE_SERIALIZER - serializer class in ``rest_auth.views.PasswordChange``, default value ``rest_auth.serializers.PasswordChangeSerializer`` + - PASSWORD_CHANGE_SERIALIZER - serializer class in ``rest_auth.views.PasswordChangeView``, default value ``rest_auth.serializers.PasswordChangeSerializer`` Example configuration: diff --git a/docs/faq.rst b/docs/faq.rst index 0a231bb..5faeee6 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -31,7 +31,7 @@ FAQ # custom fields for user company_name = models.CharField(max_length=100) - To allow update user details within one request send to rest_auth.views.UserDetails view, create serializer like this: + To allow update user details within one request send to rest_auth.views.UserDetailsView view, create serializer like this: .. code-block:: python diff --git a/docs/installation.rst b/docs/installation.rst index f15215a..cd190a4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -91,14 +91,14 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati 3. Add Social Application in django admin panel -4. Create new view as a subclass of ``rest_auth.registration.views.SocialLogin`` with ``FacebookOAuth2Adapter`` adapter as an attribute: +4. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute: .. code-block:: python from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter - from rest_auth.registration.views import SocialLogin + from rest_auth.registration.views import SocialLoginView - class FacebookLogin(SocialLogin): + class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter 5. Create url for FacebookLogin view: diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index e3c69ff..f5c444e 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -1,40 +1,99 @@ from django.http import HttpRequest from rest_framework import serializers from requests.exceptions import HTTPError -from allauth.socialaccount.helpers import complete_social_login +# Import is needed only if we are using social login, in which +# case the allauth.socialaccount will be declared +try: + from allauth.socialaccount.helpers import complete_social_login +except ImportError: + pass class SocialLoginSerializer(serializers.Serializer): + access_token = serializers.CharField(required=False) + code = serializers.CharField(required=False) - access_token = serializers.CharField(required=True) - - def validate(self, attrs): - access_token = attrs.get('access_token') - view = self.context.get('view') + def _get_request(self): request = self.context.get('request') if not isinstance(request, HttpRequest): request = request._request + return request + + def get_social_login(self, adapter, app, token, response): + """ + + :param adapter: allauth.socialaccount Adapter subclass. Usually OAuthAdapter or Auth2Adapter + :param app: `allauth.socialaccount.SocialApp` instance + :param token: `allauth.socialaccount.SocialToken` instance + :param response: Provider's response for OAuth1. Not used in the + :return: :return: A populated instance of the `allauth.socialaccount.SocialLoginView` instance + """ + request = self._get_request() + social_login = adapter.complete_login(request, app, token, response=response) + social_login.token = token + return social_login + + def validate(self, attrs): + view = self.context.get('view') + request = self._get_request() if not view: 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: + adapter_class = getattr(view, 'adapter_class', None) + if not adapter_class: raise serializers.ValidationError('Define adapter_class in view') - self.adapter = self.adapter_class() - app = self.adapter.get_provider().get_app(request) - token = self.adapter.parse_token({'access_token': access_token}) + adapter = adapter_class() + app = 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 + + # Case 1: We received the access_token + if('access_token' in attrs): + access_token = attrs.get('access_token') + + # Case 2: We received the authorization code + 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' + ) + + code = attrs.get('code') + + provider = adapter.get_provider() + scope = provider.get_scope(request) + client = self.client_class( + request, + app.client_id, + app.secret, + adapter.access_token_method, + adapter.access_token_url, + self.callback_url, + scope + ) + token = client.get_access_token(code) + access_token = token['access_token'] + + else: + raise serializers.ValidationError('Incorrect input. access_token or code is required.') + + token = adapter.parse_token({'access_token': access_token}) token.app = app try: - login = self.adapter.complete_login(request, app, token, - response=access_token) - - login.token = token + login = self.get_social_login(adapter, app, token, access_token) complete_social_login(request, login) except HTTPError: raise serializers.ValidationError('Incorrect value') diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index 838070e..abdd8b5 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -1,12 +1,12 @@ from django.views.generic import TemplateView from django.conf.urls import patterns, url -from .views import Register, VerifyEmail +from .views import RegisterView, VerifyEmailView urlpatterns = patterns( '', - url(r'^$', Register.as_view(), name='rest_register'), - url(r'^verify-email/$', VerifyEmail.as_view(), name='rest_verify_email'), + url(r'^$', RegisterView.as_view(), name='rest_register'), + url(r'^verify-email/$', VerifyEmailView.as_view(), name='rest_verify_email'), # This url is used by django-allauth and empty TemplateView is # defined just to allow reverse() call inside app, for example when email diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index f314ee8..1895267 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -3,21 +3,32 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status +from rest_framework.authtoken.models import Token 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 TokenSerializer from rest_auth.registration.serializers import SocialLoginSerializer -from rest_auth.views import Login +from rest_auth.views import LoginView -class Register(APIView, SignupView): +class RegisterView(APIView, SignupView): + """ + Accepts the credentials and creates a new user + if user does not exist already + Return the REST Token if the credentials are valid and authenticated. + Calls allauth complete_signup method + + Accept the following POST parameters: username, email, password + Return the REST Framework Token Object's key. + """ permission_classes = (AllowAny,) - user_serializer_class = UserDetailsSerializer allowed_methods = ('POST', 'OPTIONS', 'HEAD') + token_model = Token + serializer_class = TokenSerializer def get(self, *args, **kwargs): return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) @@ -27,6 +38,9 @@ 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,14 +61,15 @@ class Register(APIView, SignupView): return self.get_response_with_errors() def get_response(self): - serializer = self.user_serializer_class(instance=self.user) + # serializer = self.user_serializer_class(instance=self.user) + serializer = self.serializer_class(instance=self.token) return Response(serializer.data, status=status.HTTP_201_CREATED) def get_response_with_errors(self): return Response(self.form.errors, status=status.HTTP_400_BAD_REQUEST) -class VerifyEmail(APIView, ConfirmEmailView): +class VerifyEmailView(APIView, ConfirmEmailView): permission_classes = (AllowAny,) allowed_methods = ('POST', 'OPTIONS', 'HEAD') @@ -69,14 +84,28 @@ class VerifyEmail(APIView, ConfirmEmailView): return Response({'message': 'ok'}, status=status.HTTP_200_OK) -class SocialLogin(Login): +class SocialLoginView(LoginView): """ 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): + + class FacebookLogin(SocialLoginView): 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(SocialLoginView): + 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..9cfb6b9 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -1,4 +1,4 @@ -from django.contrib.auth import get_user_model +from django.contrib.auth import get_user_model, authenticate from django.conf import settings from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm try: @@ -7,24 +7,74 @@ except: # make compatible with django 1.5 from django.utils.http import base36_to_int as uid_decoder from django.contrib.auth.tokens import default_token_generator +from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers +from rest_framework import serializers, exceptions from rest_framework.authtoken.models import Token -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) + + elif 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) + + # If required, is the email verified? 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.') + + attrs['user'] = user return attrs diff --git a/rest_auth/test_urls.py b/rest_auth/test_urls.py index e714598..ae5ef1d 100644 --- a/rest_auth/test_urls.py +++ b/rest_auth/test_urls.py @@ -5,10 +5,10 @@ import rest_auth.django_test_urls from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter from rest_auth.urls import urlpatterns -from rest_auth.registration.views import SocialLogin +from rest_auth.registration.views import SocialLoginView -class FacebookLogin(SocialLogin): +class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter urlpatterns += patterns( diff --git a/rest_auth/urls.py b/rest_auth/urls.py index be14703..d753c44 100644 --- a/rest_auth/urls.py +++ b/rest_auth/urls.py @@ -1,21 +1,21 @@ from django.conf.urls import patterns, url from rest_auth.views import ( - Login, Logout, UserDetails, PasswordChange, - PasswordReset, PasswordResetConfirm + LoginView, LogoutView, UserDetailsView, PasswordChangeView, + PasswordResetView, PasswordResetConfirmView ) urlpatterns = patterns( '', # URLs that do not require a session or valid token - url(r'^password/reset/$', PasswordReset.as_view(), + url(r'^password/reset/$', PasswordResetView.as_view(), name='rest_password_reset'), - url(r'^password/reset/confirm/$', PasswordResetConfirm.as_view(), + url(r'^password/reset/confirm/$', PasswordResetConfirmView.as_view(), name='rest_password_reset_confirm'), - url(r'^login/$', Login.as_view(), name='rest_login'), + 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/$', Logout.as_view(), name='rest_logout'), - url(r'^user/$', UserDetails.as_view(), name='rest_user_details'), - url(r'^password/change/$', PasswordChange.as_view(), + 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'), ) diff --git a/rest_auth/views.py b/rest_auth/views.py index 0d18d86..d789ac4 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -16,7 +16,7 @@ from .app_settings import ( ) -class Login(GenericAPIView): +class LoginView(GenericAPIView): """ Check the credentials and return the REST Token @@ -57,7 +57,7 @@ class Login(GenericAPIView): return self.get_response() -class Logout(APIView): +class LogoutView(APIView): """ Calls Django logout method and delete the Token object @@ -79,7 +79,7 @@ class Logout(APIView): status=status.HTTP_200_OK) -class UserDetails(RetrieveUpdateAPIView): +class UserDetailsView(RetrieveUpdateAPIView): """ Returns User's details in JSON format. @@ -97,7 +97,7 @@ class UserDetails(RetrieveUpdateAPIView): return self.request.user -class PasswordReset(GenericAPIView): +class PasswordResetView(GenericAPIView): """ Calls Django Auth PasswordResetForm save method. @@ -124,7 +124,7 @@ class PasswordReset(GenericAPIView): ) -class PasswordResetConfirm(GenericAPIView): +class PasswordResetConfirmView(GenericAPIView): """ Password reset e-mail link is confirmed, therefore this resets the user's password. @@ -147,7 +147,7 @@ class PasswordResetConfirm(GenericAPIView): return Response({"success": "Password has been reset with the new password."}) -class PasswordChange(GenericAPIView): +class PasswordChangeView(GenericAPIView): """ Calls Django Auth SetPasswordForm save method.