diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..70d6d0d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,26 @@ +# .coveragerc to control coverage.py +[run] +omit=*site-packages*,*distutils*,*migrations* + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +ignore_errors = True + +[html] +directory = coverage_html \ No newline at end of file diff --git a/.gitignore b/.gitignore index 85446ba..7d8d699 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,6 @@ nosetests.xml coverage.xml coverage_html -# Translations -*.mo - # Mr Developer .mr.developer.cfg .project diff --git a/.travis.yml b/.travis.yml index 5152e3c..61a3dd1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,18 @@ language: python python: - "2.7" + - "3.5" env: - - DJANGO=1.7.7 - - DJANGO=1.8 + - DJANGO=1.8.13 + - DJANGO=1.9.7 install: - pip install -q Django==$DJANGO --use-mirrors - pip install coveralls - pip install -r rest_auth/tests/requirements.pip +matrix: + exclude: + - python: "3.5" + env: DJANGO=1.8.13 script: - coverage run --source=rest_auth setup.py test after_success: diff --git a/demo/demo/urls.py b/demo/demo/urls.py index 54d06ad..3813e5a 100644 --- a/demo/demo/urls.py +++ b/demo/demo/urls.py @@ -11,6 +11,8 @@ urlpatterns = [ name='email-verification'), url(r'^login/$', TemplateView.as_view(template_name="login.html"), name='login'), + url(r'^logout/$', TemplateView.as_view(template_name="logout.html"), + name='logout'), url(r'^password-reset/$', TemplateView.as_view(template_name="password_reset.html"), name='password-reset'), diff --git a/demo/requirements.pip b/demo/requirements.pip index f1c5057..c68a521 100644 --- a/demo/requirements.pip +++ b/demo/requirements.pip @@ -1,4 +1,4 @@ -django>=1.7.0 -django-rest-auth==0.6.0 -django-allauth==0.24.1 +django>=1.8.0 +django-rest-auth==0.8.1 +django-allauth>=0.24.1 six==1.9.0 diff --git a/demo/templates/base.html b/demo/templates/base.html index 8a0b0ed..03a2b73 100644 --- a/demo/templates/base.html +++ b/demo/templates/base.html @@ -40,6 +40,7 @@
  • User details
  • +
  • Logout
  • Password change
  • diff --git a/demo/templates/fragments/logout_form.html b/demo/templates/fragments/logout_form.html new file mode 100644 index 0000000..7fd281d --- /dev/null +++ b/demo/templates/fragments/logout_form.html @@ -0,0 +1,20 @@ +{% block content %} + +
    {% csrf_token %} +
    + +
    + +

    Token received after login

    +
    +
    + +
    +
    + +
    +
    + +
    +
    +{% endblock %} diff --git a/demo/templates/logout.html b/demo/templates/logout.html new file mode 100644 index 0000000..2ae28e2 --- /dev/null +++ b/demo/templates/logout.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block content %} +
    +

    Logout


    + {% include "fragments/logout_form.html" %} +
    +{% endblock %} diff --git a/docs/api_endpoints.rst b/docs/api_endpoints.rst index 1f9660f..05c2196 100644 --- a/docs/api_endpoints.rst +++ b/docs/api_endpoints.rst @@ -11,7 +11,11 @@ Basic - password (string) -- /rest-auth/logout/ (POST) +- /rest-auth/logout/ (POST, GET) + + .. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET (this is the exact same conf from allauth) + + - token - /rest-auth/password/reset/ (POST) @@ -70,3 +74,8 @@ Basing on example from installation section :doc:`Installation ` - access_token - code + +- /rest-auth/twitter/ (POST) + + - access_token + - token_secret diff --git a/docs/changelog.rst b/docs/changelog.rst index 735b936..09201f4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,18 @@ Changelog ========= +0.8.0 +----- +- added support for django-rest-framework-jwt +- bugfixes + +0.7.0 +----- +- Wrapped API returned strings in ugettext_lazy +- Fixed not using ``get_username`` which caused issues when using custom user model without username field +- Django 1.9 support +- Added ``TwitterLoginSerializer`` + 0.6.0 ----- - dropped support for Python 2.6 diff --git a/docs/configuration.rst b/docs/configuration.rst index 3746234..7e4f280 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -10,6 +10,8 @@ Configuration - TOKEN_SERIALIZER - response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.TokenSerializer`` + - JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.JWTSerializer`` + - 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.PasswordResetView``, default value ``rest_auth.serializers.PasswordResetSerializer`` @@ -29,12 +31,12 @@ Configuration ... } -- **REST_AUTH_REGISTRATION_SERIALIZERS** +- **REST_AUTH_REGISTER_SERIALIZERS** You can define your custom serializers for registration endpoint. Possible key values: - - REGISTER_SERIALIZER - serializer class in ``rest_auth.register.views.RegisterView``, default value ``rest_auth.register.serializers.RegisterSerializer`` + - REGISTER_SERIALIZER - serializer class in ``rest_auth.register.views.RegisterView``, default value ``rest_auth.registration.serializers.RegisterSerializer`` - **REST_AUTH_TOKEN_MODEL** - model class for tokens, default value ``rest_framework.authtoken.models`` @@ -42,6 +44,25 @@ Configuration - **REST_SESSION_LOGIN** - Enable session login in Login API view (default: True) +- **REST_USE_JWT** - Enable JWT Authentication instead of Token/Session based. This is built on top of django-rest-framework-jwt http://getblimp.github.io/django-rest-framework-jwt/, which must also be installed. (default: False) + - **OLD_PASSWORD_FIELD_ENABLED** - set it to True if you want to have old password verification on password change enpoint (default: False) - **LOGOUT_ON_PASSWORD_CHANGE** - set to False if you want to keep the current user logged in after a password change + + +Throttling +============= + +You may specify custom throttling for ``rest_auth.register.views.RegisterView`` by specifying DRF settings: + + .. code-block:: python + + REST_FRAMEWORK = { + 'DEFAULT_THROTTLE_RATES': { + 'anon': '6/m', + 'register_view':'1/h', + }, + } + + diff --git a/docs/index.rst b/docs/index.rst index 2ad4a05..dc25e83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Welcome to django-rest-auth's documentation! ============================================ -.. warning:: Updating django-rest-auth to version **0.3.4** is highly recommended because of a security issue in PasswordResetConfirmation validation method. +.. warning:: Updating django-rest-auth from version **0.3.3** is highly recommended because of a security issue in PasswordResetConfirmation validation method. .. note:: django-rest-auth from v0.3.3 supports django-rest-framework v3.0 diff --git a/docs/installation.rst b/docs/installation.rst index 92251dc..6144011 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -38,18 +38,23 @@ You're good to go now! Registration (optional) ----------------------- -1. If you want to enable standard registration process you will need to install ``django-allauth`` by using ``pip install django-rest-auth[extras]`` or ``pip install django-rest-auth[with_social]``. +1. If you want to enable standard registration process you will need to install ``django-allauth`` by using ``pip install django-rest-auth[with_social]``. -2. Add ``allauth``, ``allauth.account`` and ``rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py: +2. Add ``django.contrib.sites``, ``allauth``, ``allauth.account`` and ``rest_auth.registration`` apps to INSTALLED_APPS in your django settings.py: + +3. Add ``SITE_ID = 1`` to your django settings.py .. code-block:: python INSTALLED_APPS = ( ..., + 'django.contrib.sites', 'allauth', 'allauth.account', 'rest_auth.registration', ) + + SITE_ID = 1 3. Add rest_auth.registration urls: @@ -65,11 +70,11 @@ Registration (optional) Social Authentication (optional) -------------------------------- -Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view. Below is an example with Facebook authentication. +Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creating social media authentication view. -.. note:: Points 1, 2 and 3 are related with ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 4. See ``django-allauth`` documentation for more details. +.. note:: Points 1 and 2 are related to ``django-allauth`` configuration, so if you have already configured social authentication, then please go to step 3. See ``django-allauth`` documentation for more details. -1. Add ``allauth.socialaccount`` and ``allauth.socialaccount.providers.facebook`` apps to INSTALLED_APPS in your django settings.py: +1. Add ``allauth.socialaccount`` and ``allauth.socialaccount.providers.facebook`` or ``allauth.socialaccount.providers.twitter`` apps to INSTALLED_APPS in your django settings.py: .. code-block:: python @@ -79,16 +84,22 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati 'rest_framework.authtoken', 'rest_auth' ..., + 'django.contrib.sites', 'allauth', 'allauth.account', 'rest_auth.registration', ..., 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', + 'allauth.socialaccount.providers.twitter', + ) 2. Add Social Application in django admin panel +Facebook +######## + 3. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute: .. code-block:: python @@ -103,9 +114,50 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati .. code-block:: python - urlpatterns += pattern('', + urlpatterns += patterns('', ..., url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login') ) + +Twitter +####### + +If you are using Twitter for your social authentication, it is a bit different since Twitter uses OAuth 1.0. + +3. Create new view as a subclass of ``rest_auth.views.LoginView`` with ``TwitterOAuthAdapter`` adapter and ``TwitterLoginSerializer`` as an attribute: + +.. code-block:: python + + from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter + from rest_auth.views import LoginView + from rest_auth.social_serializers import TwitterLoginSerializer + + class TwitterLogin(LoginView): + serializer_class = TwitterLoginSerializer + adapter_class = TwitterOAuthAdapter + +4. Create url for TwitterLogin view: + +.. code-block:: python + + urlpatterns += patterns('', + ..., + url(r'^rest-auth/twitter/$', TwitterLogin.as_view(), name='twitter_login') + ) .. note:: Starting from v0.21.0, django-allauth has dropped support for context processors. Check out http://django-allauth.readthedocs.org/en/latest/changelog.html#from-0-21-0 for more details. + + +JWT Support (optional) +---------------------- + +By default, ``django-rest-auth`` uses Django's Token-based authentication. If you want to use JWT authentication, you need to install the following: + +1. Install ``django-rest-framework-jwt`` http://getblimp.github.io/django-rest-framework-jwt/ . Right now this is the only supported JWT library. + +2. Add the following to your settings + +.. code-block:: python + + REST_USE_JWT = True + diff --git a/rest_auth/app_settings.py b/rest_auth/app_settings.py index b77d1d2..1b75fe6 100644 --- a/rest_auth/app_settings.py +++ b/rest_auth/app_settings.py @@ -2,6 +2,7 @@ from django.conf import settings from rest_auth.serializers import ( TokenSerializer as DefaultTokenSerializer, + JWTSerializer as DefaultJWTSerializer, UserDetailsSerializer as DefaultUserDetailsSerializer, LoginSerializer as DefaultLoginSerializer, PasswordResetSerializer as DefaultPasswordResetSerializer, @@ -17,6 +18,9 @@ serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {}) TokenSerializer = import_callable( serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer)) +JWTSerializer = import_callable( + serializers.get('JWT_SERIALIZER', DefaultJWTSerializer)) + UserDetailsSerializer = import_callable( serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer) ) diff --git a/rest_auth/locale/de/LC_MESSAGES/django.mo b/rest_auth/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000..07861ff Binary files /dev/null and b/rest_auth/locale/de/LC_MESSAGES/django.mo differ diff --git a/rest_auth/locale/de/LC_MESSAGES/django.po b/rest_auth/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..3ad22d0 --- /dev/null +++ b/rest_auth/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,99 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-02-02 14:11+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: registration/serializers.py:54 +msgid "View is not defined, pass it as a context variable" +msgstr "\"View\" ist nicht definiert, übergib es als Contextvariable" + +#: registration/serializers.py:59 +msgid "Define adapter_class in view" +msgstr "Definier \"adapter_class\" in view" + +#: registration/serializers.py:78 +msgid "Define callback_url in view" +msgstr "Definier \"callback_url\" in view" + +#: registration/serializers.py:82 +msgid "Define client_class in view" +msgstr "Definier \"client_class\" in view" + +#: registration/serializers.py:102 +msgid "Incorrect input. access_token or code is required." +msgstr "Falsche Eingabe. \"access_token\" oder \"code\" erforderlich." + +#: registration/serializers.py:111 +msgid "Incorrect value" +msgstr "Falscher Wert." + +#: registration/serializers.py:140 +msgid "A user is already registered with this e-mail address." +msgstr "Ein User mit dieser E-Mail Adresse ist schon registriert." + +#: registration/serializers.py:148 +msgid "The two password fields didn't match." +msgstr "Die beiden Passwörter sind nicht identisch." + +#: registration/views.py:64 +msgid "ok" +msgstr "Ok" + +#: serializers.py:29 +msgid "Must include \"email\" and \"password\"." +msgstr "Muss \"email\" und \"password\" enthalten." + +#: serializers.py:40 +msgid "Must include \"username\" and \"password\"." +msgstr "Muss \"username\" und \"password\" enthalten." + +#: serializers.py:53 +msgid "Must include either \"username\" or \"email\" and \"password\"." +msgstr "Muss entweder \"username\" oder \"email\" und password \"password\"" + +#: serializers.py:94 +msgid "User account is disabled." +msgstr "Der Useraccount ist deaktiviert." + +#: serializers.py:97 +msgid "Unable to log in with provided credentials." +msgstr "Kann nicht mit den angegeben Zugangsdaten anmelden." + +#: serializers.py:106 +msgid "E-mail is not verified." +msgstr "E-Mail Adresse ist nicht verifiziert." + +#: serializers.py:152 +msgid "Error" +msgstr "Fehler" + +#: views.py:71 +msgid "Successfully logged out." +msgstr "Erfolgreich ausgeloggt." + +#: views.py:111 +msgid "Password reset e-mail has been sent." +msgstr "Die E-Mail zum Zurücksetzen des Passwortes wurde verschickt." + +#: views.py:132 +msgid "Password has been reset with the new password." +msgstr "Das Passwort wurde mit dem neuen Passwort ersetzt." + +#: views.py:150 +msgid "New password has been saved." +msgstr "Das neue Passwort wurde gespeichert." diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py index e3c83fb..9056164 100644 --- a/rest_auth/registration/serializers.py +++ b/rest_auth/registration/serializers.py @@ -1,5 +1,6 @@ from django.http import HttpRequest from django.conf import settings +from django.utils.translation import ugettext_lazy as _ try: from allauth.account import app_settings as allauth_settings @@ -14,13 +15,8 @@ from rest_framework import serializers from requests.exceptions import HTTPError # Import is needed only if we are using social login, in which # case the allauth.socialaccount will be declared - if 'allauth.socialaccount' in settings.INSTALLED_APPS: - try: - from allauth.socialaccount.helpers import complete_social_login - except ImportError: - pass - + from allauth.socialaccount.helpers import complete_social_login class SocialLoginSerializer(serializers.Serializer): @@ -53,14 +49,14 @@ class SocialLoginSerializer(serializers.Serializer): if not view: raise serializers.ValidationError( - 'View is not defined, pass it as a context variable' + _('View is not defined, pass it as a context variable') ) adapter_class = getattr(view, 'adapter_class', None) if not adapter_class: - raise serializers.ValidationError('Define adapter_class in view') + raise serializers.ValidationError(_('Define adapter_class in view')) - adapter = adapter_class() + adapter = adapter_class(request) app = adapter.get_provider().get_app(request) # More info on code vs access_token @@ -77,11 +73,11 @@ class SocialLoginSerializer(serializers.Serializer): if not self.callback_url: raise serializers.ValidationError( - 'Define callback_url in view' + _('Define callback_url in view') ) if not self.client_class: raise serializers.ValidationError( - 'Define client_class in view' + _('Define client_class in view') ) code = attrs.get('code') @@ -101,7 +97,7 @@ class SocialLoginSerializer(serializers.Serializer): access_token = token['access_token'] else: - raise serializers.ValidationError('Incorrect input. access_token or code is required.') + raise serializers.ValidationError(_('Incorrect input. access_token or code is required.')) token = adapter.parse_token({'access_token': access_token}) token.app = app @@ -110,7 +106,7 @@ class SocialLoginSerializer(serializers.Serializer): login = self.get_social_login(adapter, app, token, access_token) complete_social_login(request, login) except HTTPError: - raise serializers.ValidationError('Incorrect value') + raise serializers.ValidationError(_('Incorrect value')) if not login.is_existing: login.lookup() @@ -139,7 +135,7 @@ class RegisterSerializer(serializers.Serializer): if allauth_settings.UNIQUE_EMAIL: if email and email_address_exists(email): raise serializers.ValidationError( - "A user is already registered with this e-mail address.") + _("A user is already registered with this e-mail address.")) return email def validate_password1(self, password): @@ -147,7 +143,7 @@ class RegisterSerializer(serializers.Serializer): def validate(self, data): if data['password1'] != data['password2']: - raise serializers.ValidationError("The two password fields didn't match.") + raise serializers.ValidationError(_("The two password fields didn't match.")) return data def custom_signup(self, request, user): diff --git a/rest_auth/registration/urls.py b/rest_auth/registration/urls.py index 9e56c3b..8f4d0a2 100644 --- a/rest_auth/registration/urls.py +++ b/rest_auth/registration/urls.py @@ -17,7 +17,7 @@ urlpatterns = [ # with proper key. # If you don't want to use API on that step, then just use ConfirmEmailView # view from: - # djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 - url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), + # django-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 + url(r'^account-confirm-email/(?P[-:\w]+)/$', TemplateView.as_view(), name='account_confirm_email'), ] diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py index fa95e7d..3a205aa 100644 --- a/rest_auth/registration/views.py +++ b/rest_auth/registration/views.py @@ -1,15 +1,19 @@ +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework.generics import CreateAPIView from rest_framework import status -from rest_framework.exceptions import MethodNotAllowed +from allauth.account.adapter import get_adapter from allauth.account.views import ConfirmEmailView from allauth.account.utils import complete_signup from allauth.account import app_settings as allauth_settings from rest_auth.app_settings import (TokenSerializer, + JWTSerializer, create_token) from rest_auth.registration.serializers import (SocialLoginSerializer, VerifyEmailSerializer) @@ -17,18 +21,28 @@ from rest_auth.views import LoginView from rest_auth.models import TokenModel from .app_settings import RegisterSerializer +from rest_auth.utils import jwt_encode + class RegisterView(CreateAPIView): serializer_class = RegisterSerializer permission_classes = (AllowAny, ) token_model = TokenModel + throttle_scope = 'register_view' def get_response_data(self, user): if allauth_settings.EMAIL_VERIFICATION == \ allauth_settings.EmailVerificationMethod.MANDATORY: return {} - return TokenSerializer(user.auth_token).data + if getattr(settings, 'REST_USE_JWT', False): + data = { + 'user': user, + 'token': self.token + } + return JWTSerializer(data).data + else: + return TokenSerializer(user.auth_token).data def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -40,7 +54,11 @@ class RegisterView(CreateAPIView): def perform_create(self, serializer): user = serializer.save(self.request) - create_token(self.token_model, user, serializer) + if getattr(settings, 'REST_USE_JWT', False): + self.token = jwt_encode(user) + else: + create_token(self.token_model, user, serializer) + complete_signup(self.request._request, user, allauth_settings.EMAIL_VERIFICATION, None) @@ -52,16 +70,13 @@ class VerifyEmailView(APIView, ConfirmEmailView): permission_classes = (AllowAny,) allowed_methods = ('POST', 'OPTIONS', 'HEAD') - def get(self, *args, **kwargs): - raise MethodNotAllowed('GET') - def post(self, request, *args, **kwargs): serializer = VerifyEmailSerializer(data=request.data) serializer.is_valid(raise_exception=True) self.kwargs['key'] = serializer.validated_data['key'] confirmation = self.get_object() confirmation.confirm(self.request) - return Response({'message': 'ok'}, status=status.HTTP_200_OK) + return Response({'message': _('ok')}, status=status.HTTP_200_OK) class SocialLoginView(LoginView): @@ -89,3 +104,6 @@ class SocialLoginView(LoginView): """ serializer_class = SocialLoginSerializer + + def process_login(self): + get_adapter(self.request).login(self.request, self.user) diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py index 817e561..3d4faae 100644 --- a/rest_auth/serializers.py +++ b/rest_auth/serializers.py @@ -103,7 +103,7 @@ class LoginSerializer(serializers.Serializer): if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: email_address = user.emailaddress_set.get(email=user.email) if not email_address.verified: - raise serializers.ValidationError('E-mail is not verified.') + raise serializers.ValidationError(_('E-mail is not verified.')) attrs['user'] = user return attrs @@ -130,6 +130,14 @@ class UserDetailsSerializer(serializers.ModelSerializer): read_only_fields = ('email', ) +class JWTSerializer(serializers.Serializer): + """ + Serializer for JWT authentication. + """ + token = serializers.CharField() + user = UserDetailsSerializer() + + class PasswordResetSerializer(serializers.Serializer): """ @@ -149,7 +157,7 @@ class PasswordResetSerializer(serializers.Serializer): # Create PasswordResetForm with the serializer self.reset_form = self.password_reset_form_class(data=self.initial_data) if not self.reset_form.is_valid(): - raise serializers.ValidationError(_('Error')) + raise serializers.ValidationError(self.reset_form.errors) return value diff --git a/rest_auth/social_serializers.py b/rest_auth/social_serializers.py new file mode 100644 index 0000000..6e06be5 --- /dev/null +++ b/rest_auth/social_serializers.py @@ -0,0 +1,73 @@ +from django.conf import settings +from django.http import HttpRequest +from rest_framework import serializers +# Import is needed only if we are using social login, in which +# case the allauth.socialaccount will be declared +if 'allauth.socialaccount' in settings.INSTALLED_APPS: + from allauth.socialaccount.helpers import complete_social_login + from allauth.socialaccount.models import SocialToken + from allauth.socialaccount.providers.oauth.client import OAuthError + + +class TwitterLoginSerializer(serializers.Serializer): + access_token = serializers.CharField(required=True) + token_secret = serializers.CharField(required=True) + + 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' + ) + + adapter_class = getattr(view, 'adapter_class', None) + if not adapter_class: + raise serializers.ValidationError('Define adapter_class in view') + + adapter = adapter_class(request) + app = adapter.get_provider().get_app(request) + + access_token = attrs.get('access_token') + token_secret = attrs.get('token_secret') + + request.session['oauth_api.twitter.com_access_token'] = { + 'oauth_token': access_token, + 'oauth_token_secret': token_secret, + } + token = SocialToken(token=access_token, token_secret=token_secret) + token.app = app + + try: + login = self.get_social_login(adapter, app, token, access_token) + complete_social_login(request, login) + except OAuthError as e: + raise serializers.ValidationError(str(e)) + + if not login.is_existing: + login.lookup() + login.save(request, connect=True) + attrs['user'] = login.account.user + + return attrs diff --git a/rest_auth/tests/django_urls.py b/rest_auth/tests/django_urls.py index 2193f1b..c1fb050 100644 --- a/rest_auth/tests/django_urls.py +++ b/rest_auth/tests/django_urls.py @@ -1,79 +1,16 @@ # Moved in Django 1.8 from django to tests/auth_tests/urls.py -from django.conf.urls import include, url -from django.contrib import admin +from django.conf.urls import url from django.contrib.auth import views from django.contrib.auth.decorators import login_required -from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.urls import urlpatterns -from django.contrib.messages.api import info -from django.http import HttpRequest, HttpResponse -from django.shortcuts import render -from django.template import RequestContext, Template -from django.views.decorators.cache import never_cache -class CustomRequestAuthenticationForm(AuthenticationForm): - def __init__(self, request, *args, **kwargs): - assert isinstance(request, HttpRequest) - super(CustomRequestAuthenticationForm, self).__init__(request, *args, **kwargs) - - -@never_cache -def remote_user_auth_view(request): - """ - Dummy view for remote user tests - """ - t = Template("Username is {{ user }}.") - c = RequestContext(request, {}) - return HttpResponse(t.render(c)) - - -def auth_processor_no_attr_access(request): - render(request, 'context_processors/auth_attrs_no_access.html') - # *After* rendering, we check whether the session was accessed - return render(request, - 'context_processors/auth_attrs_test_access.html', - {'session_accessed': request.session.accessed}) - - -def auth_processor_attr_access(request): - render(request, 'context_processors/auth_attrs_access.html') - return render(request, - 'context_processors/auth_attrs_test_access.html', - {'session_accessed': request.session.accessed}) - - -def auth_processor_user(request): - return render(request, 'context_processors/auth_attrs_user.html') - - -def auth_processor_perms(request): - return render(request, 'context_processors/auth_attrs_perms.html') - - -def auth_processor_perm_in_perms(request): - return render(request, 'context_processors/auth_attrs_perm_in_perms.html') - - -def auth_processor_messages(request): - info(request, "Message 1") - return render(request, 'context_processors/auth_attrs_messages.html') - - -def userpage(request): - pass - - -def custom_request_auth_login(request): - return views.login(request, authentication_form=CustomRequestAuthenticationForm) - # special urls for auth test cases urlpatterns += [ url(r'^logout/custom_query/$', views.logout, dict(redirect_field_name='follow')), url(r'^logout/next_page/$', views.logout, dict(next_page='/somewhere/')), url(r'^logout/next_page/named/$', views.logout, dict(next_page='password_reset')), - url(r'^remote_user/$', remote_user_auth_view), url(r'^password_reset_from_email/$', views.password_reset, dict(from_email='staffmember@example.com')), url(r'^password_reset/custom_redirect/$', views.password_reset, dict(post_reset_redirect='/custom/')), url(r'^password_reset/custom_redirect/named/$', views.password_reset, dict(post_reset_redirect='password_reset')), @@ -90,16 +27,4 @@ urlpatterns += [ url(r'^admin_password_reset/$', views.password_reset, dict(is_admin_site=True)), url(r'^login_required/$', login_required(views.password_reset)), url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')), - - url(r'^auth_processor_no_attr_access/$', auth_processor_no_attr_access), - url(r'^auth_processor_attr_access/$', auth_processor_attr_access), - url(r'^auth_processor_user/$', auth_processor_user), - url(r'^auth_processor_perms/$', auth_processor_perms), - url(r'^auth_processor_perm_in_perms/$', auth_processor_perm_in_perms), - url(r'^auth_processor_messages/$', auth_processor_messages), - url(r'^custom_request_auth_login/$', custom_request_auth_login), - url(r'^userpage/(.+)/$', userpage, name="userpage"), - - # This line is only required to render the password reset with is_admin=True - url(r'^admin/', include(admin.site.urls)), ] diff --git a/rest_auth/tests/requirements.pip b/rest_auth/tests/requirements.pip index bb8d844..de66892 100644 --- a/rest_auth/tests/requirements.pip +++ b/rest_auth/tests/requirements.pip @@ -1,3 +1,4 @@ django-allauth>=0.19.1 responses>=0.3.0 flake8==2.4.0 +djangorestframework-jwt>=1.7.2 diff --git a/rest_auth/tests/settings.py b/rest_auth/tests/settings.py index b09b496..060cc89 100644 --- a/rest_auth/tests/settings.py +++ b/rest_auth/tests/settings.py @@ -45,6 +45,27 @@ TEMPLATE_CONTEXT_PROCESSORS = [ "allauth.socialaccount.context_processors.socialaccount", ] +# avoid deprecation warnings during tests +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + # insert your TEMPLATE_DIRS here + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': TEMPLATE_CONTEXT_PROCESSORS, + }, + }, +] + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', + ) +} + INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -59,14 +80,24 @@ INSTALLED_APPS = [ 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', + 'allauth.socialaccount.providers.twitter', 'rest_framework', 'rest_framework.authtoken', 'rest_auth', - 'rest_auth.registration' + 'rest_auth.registration', + + 'rest_framework_jwt' ] SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" ACCOUNT_ACTIVATION_DAYS = 1 SITE_ID = 1 + +AUTHENTICATION_BACKENDS = ( + # Needed to login by username in Django admin, regardless of `allauth` + 'django.contrib.auth.backends.ModelBackend', + # `allauth` specific authentication methods, such as login by e-mail + 'allauth.account.auth_backends.AuthenticationBackend', +) diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py index f6cc839..a24e9f0 100644 --- a/rest_auth/tests/test_api.py +++ b/rest_auth/tests/test_api.py @@ -1,16 +1,16 @@ from django.core.urlresolvers import reverse -from django.test import TestCase +from django.test import TestCase, override_settings from django.contrib.auth import get_user_model from django.core import mail from django.conf import settings -from django.test.utils import override_settings from django.utils.encoding import force_text from rest_framework import status - +from allauth.account import app_settings as account_app_settings from .test_base import BaseAPITestCase +@override_settings(ROOT_URLCONF="tests.urls") class APITestCase1(TestCase, BaseAPITestCase): """ Case #1: @@ -18,7 +18,7 @@ class APITestCase1(TestCase, BaseAPITestCase): - custom registration: backend defined """ - urls = 'tests.urls' + # urls = 'tests.urls' USERNAME = 'person' PASS = 'person' @@ -57,7 +57,36 @@ class APITestCase1(TestCase, BaseAPITestCase): result['token'] = default_token_generator.make_token(user) return result - def test_login(self): + @override_settings(ACCOUNT_AUTHENTICATION_METHOD=account_app_settings.AuthenticationMethod.EMAIL) + def test_login_failed_email_validation(self): + payload = { + "email": '', + "password": self.PASS + } + + resp = self.post(self.login_url, data=payload, status_code=400) + self.assertEqual(resp.json['non_field_errors'][0], u'Must include "email" and "password".') + + @override_settings(ACCOUNT_AUTHENTICATION_METHOD=account_app_settings.AuthenticationMethod.USERNAME) + def test_login_failed_username_validation(self): + payload = { + "username": '', + "password": self.PASS + } + + resp = self.post(self.login_url, data=payload, status_code=400) + self.assertEqual(resp.json['non_field_errors'][0], u'Must include "username" and "password".') + + @override_settings(ACCOUNT_AUTHENTICATION_METHOD=account_app_settings.AuthenticationMethod.USERNAME_EMAIL) + def test_login_failed_username_email_validation(self): + payload = { + "password": self.PASS + } + + resp = self.post(self.login_url, data=payload, status_code=400) + self.assertEqual(resp.json['non_field_errors'][0], u'Must include either "username" or "email" and "password".') + + def test_allauth_login_with_username(self): payload = { "username": self.USERNAME, "password": self.PASS @@ -91,6 +120,34 @@ class APITestCase1(TestCase, BaseAPITestCase): # test empty payload self.post(self.login_url, data={}, status_code=400) + @override_settings(ACCOUNT_AUTHENTICATION_METHOD=account_app_settings.AuthenticationMethod.EMAIL) + def test_allauth_login_with_email(self): + payload = { + "email": self.EMAIL, + "password": self.PASS + } + # there is no users in db so it should throw error (400) + self.post(self.login_url, data=payload, status_code=400) + + self.post(self.password_change_url, status_code=403) + + # create user + get_user_model().objects.create_user(self.EMAIL, email=self.EMAIL, password=self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + + @override_settings(REST_USE_JWT=True) + def test_login_jwt(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + self.assertEqual('token' in self.response.json.keys(), True) + self.token = self.response.json['token'] + def test_login_by_email(self): # starting test without allauth app settings.INSTALLED_APPS.remove('allauth') @@ -136,6 +193,9 @@ class APITestCase1(TestCase, BaseAPITestCase): # test empty payload self.post(self.login_url, data={}, status_code=400) + # bring back allauth + settings.INSTALLED_APPS.append('allauth') + def test_password_change(self): login_payload = { "username": self.USERNAME, @@ -307,6 +367,21 @@ class APITestCase1(TestCase, BaseAPITestCase): self.assertEqual(user.last_name, self.response.json['last_name']) self.assertEqual(user.email, self.response.json['email']) + @override_settings(REST_USE_JWT=True) + def test_user_details_using_jwt(self): + user = get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS) + payload = { + "username": self.USERNAME, + "password": self.PASS + } + self.post(self.login_url, data=payload, status_code=200) + self.token = self.response.json['token'] + self.get(self.user_url, status_code=200) + + self.patch(self.user_url, data=self.BASIC_USER_DATA, status_code=200) + user = get_user_model().objects.get(pk=user.pk) + self.assertEqual(user.email, self.response.json['email']) + def test_registration(self): user_count = get_user_model().objects.all().count() @@ -323,6 +398,19 @@ class APITestCase1(TestCase, BaseAPITestCase): self._login() self._logout() + @override_settings(REST_USE_JWT=True) + def test_registration_with_jwt(self): + user_count = get_user_model().objects.all().count() + + self.post(self.register_url, data={}, status_code=400) + + result = self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201) + self.assertIn('token', result.data) + self.assertEqual(get_user_model().objects.all().count(), user_count + 1) + + self._login() + self._logout() + def test_registration_with_invalid_password(self): data = self.REGISTRATION_DATA.copy() data['password2'] = 'foobar' @@ -331,7 +419,8 @@ class APITestCase1(TestCase, BaseAPITestCase): @override_settings( ACCOUNT_EMAIL_VERIFICATION='mandatory', - ACCOUNT_EMAIL_REQUIRED=True + ACCOUNT_EMAIL_REQUIRED=True, + ACCOUNT_EMAIL_CONFIRMATION_HMAC=False ) def test_registration_with_email_verification(self): user_count = get_user_model().objects.all().count() @@ -378,3 +467,29 @@ class APITestCase1(TestCase, BaseAPITestCase): # try to login again self._login() self._logout() + + @override_settings(ACCOUNT_LOGOUT_ON_GET=True) + def test_logout_on_get(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + + # create user + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.post(self.login_url, data=payload, status_code=200) + self.get(self.logout_url, status=status.HTTP_200_OK) + + @override_settings(ACCOUNT_LOGOUT_ON_GET=False) + def test_logout_on_post_only(self): + payload = { + "username": self.USERNAME, + "password": self.PASS + } + + # create user + get_user_model().objects.create_user(self.USERNAME, '', self.PASS) + + self.post(self.login_url, data=payload, status_code=status.HTTP_200_OK) + self.get(self.logout_url, status_code=status.HTTP_405_METHOD_NOT_ALLOWED) diff --git a/rest_auth/tests/test_base.py b/rest_auth/tests/test_base.py index ed8ffeb..faaf7bb 100644 --- a/rest_auth/tests/test_base.py +++ b/rest_auth/tests/test_base.py @@ -37,17 +37,22 @@ class BaseAPITestCase(object): # check_headers = kwargs.pop('check_headers', True) if hasattr(self, 'token'): - kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token + if getattr(settings, 'REST_USE_JWT', False): + kwargs['HTTP_AUTHORIZATION'] = 'JWT %s' % self.token + else: + kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token self.response = request_func(*args, **kwargs) is_json = bool( [x for x in self.response._headers['content-type'] if 'json' in x]) + + self.response.json = {} if is_json and self.response.content: self.response.json = json.loads(force_text(self.response.content)) - else: - self.response.json = {} + if status_code: self.assertEqual(self.response.status_code, status_code) + return self.response def post(self, *args, **kwargs): @@ -94,6 +99,9 @@ class BaseAPITestCase(object): self.user_url = reverse('rest_user_details') self.veirfy_email_url = reverse('rest_verify_email') self.fb_login_url = reverse('fb_login') + self.tw_login_url = reverse('tw_login') + self.tw_login_no_view_url = reverse('tw_login_no_view') + self.tw_login_no_adapter_url = reverse('tw_login_no_adapter') def _login(self): payload = { diff --git a/rest_auth/tests/test_social.py b/rest_auth/tests/test_social.py index 19509ef..56bdace 100644 --- a/rest_auth/tests/test_social.py +++ b/rest_auth/tests/test_social.py @@ -1,3 +1,5 @@ +import json + from django.test import TestCase from django.contrib.auth import get_user_model from django.test.utils import override_settings @@ -12,10 +14,9 @@ from rest_framework import status from .test_base import BaseAPITestCase +@override_settings(ROOT_URLCONF="tests.urls") class TestSocialAuth(TestCase, BaseAPITestCase): - urls = 'tests.urls' - USERNAME = 'person' PASS = 'person' EMAIL = "person1@world.com" @@ -35,9 +36,19 @@ class TestSocialAuth(TestCase, BaseAPITestCase): client_id='123123123', secret='321321321', ) + + twitter_social_app = SocialApp.objects.create( + provider='twitter', + name='Twitter', + client_id='11223344', + secret='55667788', + ) + site = Site.objects.get_current() social_app.sites.add(site) + twitter_social_app.sites.add(site) self.graph_api_url = GRAPH_API_URL + '/me' + self.twitter_url = 'http://twitter.com/foobarme' @responses.activate def test_failed_social_auth(self): @@ -58,11 +69,24 @@ class TestSocialAuth(TestCase, BaseAPITestCase): @responses.activate def test_social_auth(self): # fake response for facebook call - resp_body = '{"id":"123123123123","first_name":"John","gender":"male","last_name":"Smith","link":"https:\\/\\/www.facebook.com\\/john.smith","locale":"en_US","name":"John Smith","timezone":2,"updated_time":"2014-08-13T10:14:38+0000","username":"john.smith","verified":true}' # noqa + resp_body = { + "id": "123123123123", + "first_name": "John", + "gender": "male", + "last_name": "Smith", + "link": "https://www.facebook.com/john.smith", + "locale": "en_US", + "name": "John Smith", + "timezone": 2, + "updated_time": "2014-08-13T10:14:38+0000", + "username": "john.smith", + "verified": True + } + responses.add( responses.GET, self.graph_api_url, - body=resp_body, + body=json.dumps(resp_body), status=200, content_type='application/json' ) @@ -81,18 +105,146 @@ class TestSocialAuth(TestCase, BaseAPITestCase): self.assertIn('key', self.response.json.keys()) self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + def _twitter_social_auth(self): + # fake response for twitter call + resp_body = { + "id": "123123123123", + } + + responses.add( + responses.GET, + 'https://api.twitter.com/1.1/account/verify_credentials.json', + body=json.dumps(resp_body), + status=200, + content_type='application/json' + ) + + users_count = get_user_model().objects.all().count() + payload = { + 'access_token': 'abc123', + 'token_secret': '1111222233334444' + } + + self.post(self.tw_login_url, data=payload) + + self.assertIn('key', self.response.json.keys()) + self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + + # make sure that second request will not create a new user + self.post(self.tw_login_url, data=payload, status_code=200) + self.assertIn('key', self.response.json.keys()) + self.assertEqual(get_user_model().objects.all().count(), users_count + 1) + + @responses.activate + @override_settings(SOCIALACCOUNT_AUTO_SIGNUP=True) + def test_twitter_social_auth(self): + self._twitter_social_auth() + + @responses.activate + @override_settings(SOCIALACCOUNT_AUTO_SIGNUP=False) + def test_twitter_social_auth_without_auto_singup(self): + self._twitter_social_auth() + + @responses.activate + def test_twitter_social_auth_request_error(self): + # fake response for twitter call + resp_body = { + "id": "123123123123", + } + + responses.add( + responses.GET, + 'https://api.twitter.com/1.1/account/verify_credentials.json', + body=json.dumps(resp_body), + status=400, + content_type='application/json' + ) + + users_count = get_user_model().objects.all().count() + payload = { + 'access_token': 'abc123', + 'token_secret': '1111222233334444' + } + + self.post(self.tw_login_url, data=payload, status_code=400) + self.assertNotIn('key', self.response.json.keys()) + self.assertEqual(get_user_model().objects.all().count(), users_count) + + @responses.activate + def test_twitter_social_auth_no_view_in_context(self): + # fake response for twitter call + resp_body = { + "id": "123123123123", + } + + responses.add( + responses.GET, + 'https://api.twitter.com/1.1/account/verify_credentials.json', + body=json.dumps(resp_body), + status=400, + content_type='application/json' + ) + + users_count = get_user_model().objects.all().count() + payload = { + 'access_token': 'abc123', + 'token_secret': '1111222233334444' + } + + self.post(self.tw_login_no_view_url, data=payload, status_code=400) + self.assertEqual(get_user_model().objects.all().count(), users_count) + + @responses.activate + def test_twitter_social_auth_no_adapter(self): + # fake response for twitter call + resp_body = { + "id": "123123123123", + } + + responses.add( + responses.GET, + 'https://api.twitter.com/1.1/account/verify_credentials.json', + body=json.dumps(resp_body), + status=400, + content_type='application/json' + ) + + users_count = get_user_model().objects.all().count() + payload = { + 'access_token': 'abc123', + 'token_secret': '1111222233334444' + } + + self.post(self.tw_login_no_adapter_url, data=payload, status_code=400) + self.assertEqual(get_user_model().objects.all().count(), users_count) + @responses.activate @override_settings( ACCOUNT_EMAIL_VERIFICATION='mandatory', ACCOUNT_EMAIL_REQUIRED=True, - REST_SESSION_LOGIN=False + REST_SESSION_LOGIN=False, + ACCOUNT_EMAIL_CONFIRMATION_HMAC=False ) def test_edge_case(self): - resp_body = '{"id":"123123123123","first_name":"John","gender":"male","last_name":"Smith","link":"https:\\/\\/www.facebook.com\\/john.smith","locale":"en_US","name":"John Smith","timezone":2,"updated_time":"2014-08-13T10:14:38+0000","username":"john.smith","verified":true,"email":"%s"}' # noqa + resp_body = { + "id": "123123123123", + "first_name": "John", + "gender": "male", + "last_name": "Smith", + "link": "https://www.facebook.com/john.smith", + "locale": "en_US", + "name": "John Smith", + "timezone": 2, + "updated_time": "2014-08-13T10:14:38+0000", + "username": "john.smith", + "verified": True, + "email": self.EMAIL + } + responses.add( responses.GET, self.graph_api_url, - body=resp_body % self.EMAIL, + body=json.dumps(resp_body), status=200, content_type='application/json' ) @@ -125,3 +277,28 @@ class TestSocialAuth(TestCase, BaseAPITestCase): self.post(self.fb_login_url, data=payload, status_code=200) self.assertIn('key', self.response.json.keys()) + + @responses.activate + @override_settings( + REST_USE_JWT=True + ) + def test_jwt(self): + resp_body = '{"id":"123123123123","first_name":"John","gender":"male","last_name":"Smith","link":"https:\\/\\/www.facebook.com\\/john.smith","locale":"en_US","name":"John Smith","timezone":2,"updated_time":"2014-08-13T10:14:38+0000","username":"john.smith","verified":true}' # noqa + responses.add( + responses.GET, + self.graph_api_url, + body=resp_body, + status=200, + content_type='application/json' + ) + + users_count = get_user_model().objects.all().count() + payload = { + 'access_token': 'abc123' + } + + self.post(self.fb_login_url, data=payload, status_code=200) + self.assertIn('token', self.response.json.keys()) + self.assertIn('user', self.response.json.keys()) + + self.assertEqual(get_user_model().objects.all().count(), users_count + 1) diff --git a/rest_auth/tests/urls.py b/rest_auth/tests/urls.py index d922f7f..6371218 100644 --- a/rest_auth/tests/urls.py +++ b/rest_auth/tests/urls.py @@ -3,21 +3,51 @@ from django.views.generic import TemplateView from . import django_urls from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter +from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter + +from rest_framework.decorators import api_view from rest_auth.urls import urlpatterns from rest_auth.registration.views import SocialLoginView +from rest_auth.social_serializers import TwitterLoginSerializer class FacebookLogin(SocialLoginView): adapter_class = FacebookOAuth2Adapter + +class TwitterLogin(SocialLoginView): + adapter_class = TwitterOAuthAdapter + serializer_class = TwitterLoginSerializer + + +class TwitterLoginSerializerFoo(TwitterLoginSerializer): + pass + + +@api_view(['POST']) +def twitter_login_view(request): + serializer = TwitterLoginSerializerFoo( + data={'access_token': '11223344', 'token_secret': '55667788'}, + context={'request': request} + ) + serializer.is_valid(raise_exception=True) + + +class TwitterLoginNoAdapter(SocialLoginView): + serializer_class = TwitterLoginSerializer + + urlpatterns += [ url(r'^rest-registration/', include('rest_auth.registration.urls')), url(r'^test-admin/', include(django_urls)), url(r'^account-email-verification-sent/$', TemplateView.as_view(), name='account_email_verification_sent'), - url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), + url(r'^account-confirm-email/(?P[-:\w]+)/$', TemplateView.as_view(), name='account_confirm_email'), url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login'), + url(r'^social-login/twitter/$', TwitterLogin.as_view(), name='tw_login'), + url(r'^social-login/twitter-no-view/$', twitter_login_view, name='tw_login_no_view'), + url(r'^social-login/twitter-no-adapter/$', TwitterLoginNoAdapter.as_view(), name='tw_login_no_adapter'), url(r'^accounts/', include('allauth.socialaccount.urls')) ] diff --git a/rest_auth/utils.py b/rest_auth/utils.py index e224f26..99d80b8 100644 --- a/rest_auth/utils.py +++ b/rest_auth/utils.py @@ -14,3 +14,16 @@ def import_callable(path_or_callable): def default_create_token(token_model, user, serializer): token, _ = token_model.objects.get_or_create(user=user) return token + + +def jwt_encode(user): + try: + from rest_framework_jwt.settings import api_settings + except ImportError: + raise ImportError('rest_framework_jwt needs to be installed') + + jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER + jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER + + payload = jwt_payload_handler(user) + return jwt_encode_handler(payload) diff --git a/rest_auth/views.py b/rest_auth/views.py index 3bb6f6b..0761600 100644 --- a/rest_auth/views.py +++ b/rest_auth/views.py @@ -1,6 +1,10 @@ -from django.contrib.auth import login, logout +from django.contrib.auth import ( + login as django_login, + logout as django_logout +) from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext_lazy as _ from rest_framework import status from rest_framework.views import APIView @@ -9,13 +13,17 @@ from rest_framework.generics import GenericAPIView from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.generics import RetrieveUpdateAPIView +from allauth.account import app_settings as allauth_settings + from .app_settings import ( TokenSerializer, UserDetailsSerializer, LoginSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, - PasswordChangeSerializer, create_token + PasswordChangeSerializer, JWTSerializer, create_token ) from .models import TokenModel +from .utils import jwt_encode + class LoginView(GenericAPIView): @@ -31,22 +39,47 @@ class LoginView(GenericAPIView): permission_classes = (AllowAny,) serializer_class = LoginSerializer token_model = TokenModel - response_serializer = TokenSerializer + + def process_login(self): + django_login(self.request, self.user) + + def get_response_serializer(self): + if getattr(settings, 'REST_USE_JWT', False): + response_serializer = JWTSerializer + else: + response_serializer = TokenSerializer + return response_serializer def login(self): self.user = self.serializer.validated_data['user'] - self.token = create_token(self.token_model, self.user, self.serializer) + + if getattr(settings, 'REST_USE_JWT', False): + self.token = jwt_encode(self.user) + else: + self.token = create_token(self.token_model, self.user, self.serializer) + if getattr(settings, 'REST_SESSION_LOGIN', True): - login(self.request, self.user) + self.process_login() def get_response(self): - return Response( - self.response_serializer(self.token).data, status=status.HTTP_200_OK - ) + serializer_class = self.get_response_serializer() + + if getattr(settings, 'REST_USE_JWT', False): + data = { + 'user': self.user, + 'token': self.token + } + serializer = serializer_class(instance=data, context={'request': self.request}) + else: + serializer = serializer_class(instance=self.token, context={'request': self.request}) + + return Response(serializer.data, status=status.HTTP_200_OK) def post(self, request, *args, **kwargs): + self.request = request self.serializer = self.get_serializer(data=self.request.data) self.serializer.is_valid(raise_exception=True) + self.login() return self.get_response() @@ -61,20 +94,33 @@ class LogoutView(APIView): """ permission_classes = (AllowAny,) + def get(self, request, *args, **kwargs): + try: + if allauth_settings.LOGOUT_ON_GET: + response = self.logout(request) + else: + response = self.http_method_not_allowed(request, *args, **kwargs) + except Exception as exc: + response = self.handle_exception(exc) + + return self.finalize_response(request, response, *args, **kwargs) + def post(self, request): + return self.logout(request) + + def logout(self, request): try: request.user.auth_token.delete() except (AttributeError, ObjectDoesNotExist): pass - logout(request) + django_logout(request) - return Response({"success": "Successfully logged out."}, + return Response({"success": _("Successfully logged out.")}, status=status.HTTP_200_OK) class UserDetailsView(RetrieveUpdateAPIView): - """ Returns User's details in JSON format. @@ -111,13 +157,12 @@ class PasswordResetView(GenericAPIView): serializer.save() # Return the success message with OK HTTP status return Response( - {"success": "Password reset e-mail has been sent."}, + {"success": _("Password reset e-mail has been sent.")}, status=status.HTTP_200_OK ) class PasswordResetConfirmView(GenericAPIView): - """ Password reset e-mail link is confirmed, therefore this resets the user's password. @@ -133,11 +178,10 @@ class PasswordResetConfirmView(GenericAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) 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 PasswordChangeView(GenericAPIView): - """ Calls Django Auth SetPasswordForm save method. @@ -152,4 +196,4 @@ class PasswordChangeView(GenericAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) serializer.save() - return Response({"success": "New password has been saved."}) + return Response({"success": _("New password has been saved.")}) diff --git a/setup.py b/setup.py index d85b2d2..7bada0d 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ f.close() setup( name='django-rest-auth', - version='0.6.0', + version='0.8.1', author='Sumit Chachra', author_email='chachra@tivix.com', url='http://github.com/Tivix/django-rest-auth', @@ -28,16 +28,16 @@ setup( keywords='django rest auth registration rest-framework django-registration api', zip_safe=False, install_requires=[ - 'Django>=1.7.0', + 'Django>=1.8.0', 'djangorestframework>=3.1.0', 'six>=1.9.0', ], extras_require={ - 'with_social': ['django-allauth>=0.24.1'], + 'with_social': ['django-allauth>=0.25.0'], }, tests_require=[ 'responses>=0.5.0', - 'django-allauth>=0.24.1', + 'django-allauth>=0.25.0', ], test_suite='runtests.runtests', include_package_data=True,