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 %}
+
+
+{% 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,