diff --git a/.travis.yml b/.travis.yml
index 5152e3c..f07f4a3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,8 +2,9 @@ language: python
python:
- "2.7"
env:
- - DJANGO=1.7.7
- - DJANGO=1.8
+ - DJANGO=1.7.11
+ - DJANGO=1.8.9
+ - DJANGO=1.9.2
install:
- pip install -q Django==$DJANGO --use-mirrors
- pip install coveralls
diff --git a/demo/demo/urls.py b/demo/demo/urls.py
index 5b075e6..61a7c77 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..4949275 100644
--- a/demo/requirements.pip
+++ b/demo/requirements.pip
@@ -1,4 +1,4 @@
django>=1.7.0
-django-rest-auth==0.6.0
+django-rest-auth==0.7.0
django-allauth==0.24.1
six==1.9.0
diff --git a/demo/templates/base.html b/demo/templates/base.html
index 9d513c4..c9b65f1 100644
--- a/demo/templates/base.html
+++ b/demo/templates/base.html
@@ -41,6 +41,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..a24d7f5 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,17 @@
Changelog
=========
+0.8.0
+-----
+- added support for django-rest-framework-jwt
+
+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 079c710..0728e11 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,17 +31,20 @@ 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``
+- **REST_AUTH_TOKEN_MODEL** - model class for tokens, default value ``rest_framework.authtoken.models``
+- **REST_AUTH_TOKEN_CREATOR** - callable to create tokens, default value ``rest_auth.utils.default_create_token``.
- **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 much 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)
diff --git a/docs/faq.rst b/docs/faq.rst
index 5faeee6..ff04c13 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -17,7 +17,12 @@ FAQ
djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190
-2. How can I update UserProfile assigned to User model?
+2. I get an error: Reverse for 'password_reset_confirm' not found.
+
+ You need to add `password_reset_confirm` url into your ``urls.py`` (at the top of any other included urls). Please check the ``urls.py`` module inside demo app example for more details.
+
+
+3. How can I update UserProfile assigned to User model?
Assuming you already have UserProfile model defined like this
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..9e47a47 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -65,11 +65,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
@@ -85,10 +85,15 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati
...,
'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
@@ -108,4 +113,45 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati
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 += pattern('',
+ ...,
+ 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 cd6571a..f6d97a5 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,
@@ -10,12 +11,17 @@ from rest_auth.serializers import (
EmailChangeSerializer as DefaultEmailChangeSerializer)
from .utils import import_callable
+create_token = import_callable(
+ getattr(settings, 'REST_AUTH_TOKEN_CREATOR', default_create_token))
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.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/models.py b/rest_auth/models.py
index e703865..a132f9c 100644
--- a/rest_auth/models.py
+++ b/rest_auth/models.py
@@ -1,3 +1,10 @@
-# from django.db import models
+from django.conf import settings
+
+from rest_framework.authtoken.models import Token as DefaultTokenModel
+
+from .utils import import_callable
# Register your models here.
+
+TokenModel = import_callable(
+ getattr(settings, 'REST_AUTH_TOKEN_MODEL', DefaultTokenModel))
diff --git a/rest_auth/registration/serializers.py b/rest_auth/registration/serializers.py
index b27d7bd..2390b05 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,12 @@ 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
-try:
- from allauth.socialaccount.helpers import complete_social_login
-except ImportError:
- raise ImportError('allauth.socialaccount needs to be installed.')
-if 'allauth.socialaccount' not in settings.INSTALLED_APPS:
- raise ImportError('allauth.socialaccount needs to be added to INSTALLED_APPS.')
+if 'allauth.socialaccount' in settings.INSTALLED_APPS:
+ try:
+ from allauth.socialaccount.helpers import complete_social_login
+ except ImportError:
+ pass
class SocialLoginSerializer(serializers.Serializer):
@@ -53,12 +53,12 @@ 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()
app = adapter.get_provider().get_app(request)
@@ -77,11 +77,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 +101,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 +110,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 +139,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 +147,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 3206c9e..b23fc6c 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
+ # 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'),
url(r'^change-email/$', VerifyEmailView.as_view(), name='rest_email_change'),
diff --git a/rest_auth/registration/views.py b/rest_auth/registration/views.py
index 81fd951..d1d8d37 100644
--- a/rest_auth/registration/views.py
+++ b/rest_auth/registration/views.py
@@ -1,38 +1,61 @@
+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.authtoken.models import Token
from rest_framework.exceptions import MethodNotAllowed
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
+from rest_auth.app_settings import (TokenSerializer,
+ JWTSerializer,
+ create_token)
from rest_auth.registration.serializers import (SocialLoginSerializer,
VerifyEmailSerializer)
-from .app_settings import RegisterSerializer
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
+
+ def get_response_data(self, user):
+ if allauth_settings.EMAIL_VERIFICATION == \
+ allauth_settings.EmailVerificationMethod.MANDATORY:
+ return {}
+
+ 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)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
- return Response(TokenSerializer(user.auth_token).data,
- status=status.HTTP_201_CREATED,
- headers=headers)
+
+ return Response(self.get_response_data(user), status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
user = serializer.save(self.request)
- Token.objects.get_or_create(user=user)
+ 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)
@@ -53,7 +76,7 @@ class VerifyEmailView(APIView, ConfirmEmailView):
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):
diff --git a/rest_auth/serializers.py b/rest_auth/serializers.py
index 80fb254..7b14182 100644
--- a/rest_auth/serializers.py
+++ b/rest_auth/serializers.py
@@ -6,8 +6,9 @@ from django.utils.http import urlsafe_base64_decode as uid_decoder
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
+from .models import TokenModel
+
from rest_framework import serializers, exceptions
-from rest_framework.authtoken.models import Token
from rest_framework.exceptions import ValidationError
# Get the UserModel
@@ -80,7 +81,7 @@ class LoginSerializer(serializers.Serializer):
# Authentication without using allauth
if email:
try:
- username = UserModel.objects.get(email__iexact=email).username
+ username = UserModel.objects.get(email__iexact=email).get_username()
except UserModel.DoesNotExist:
pass
@@ -102,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
@@ -114,8 +115,9 @@ class TokenSerializer(serializers.ModelSerializer):
"""
class Meta:
- model = Token
- fields = ('key')
+
+ model = TokenModel
+ fields = ('key',)
class UserDetailsSerializer(serializers.ModelSerializer):
@@ -127,6 +129,12 @@ class UserDetailsSerializer(serializers.ModelSerializer):
model = UserModel
fields = ('username', 'email', 'first_name', 'last_name')
+class JWTSerializer(serializers.Serializer):
+ """
+ Serializer for JWT authentication.
+ """
+ token = serializers.CharField()
+ user = UserDetailsSerializer()
class PasswordResetSerializer(serializers.Serializer):
@@ -138,15 +146,17 @@ class PasswordResetSerializer(serializers.Serializer):
password_reset_form_class = PasswordResetForm
+ def get_email_options(self):
+ """ Override this method to change default e-mail options
+ """
+ return {}
+
def validate_email(self, value):
# 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'))
- if not UserModel.objects.filter(email__iexact=value).exists():
- raise serializers.ValidationError(_('Invalid e-mail address'))
-
return value
def save(self):
@@ -157,6 +167,8 @@ class PasswordResetSerializer(serializers.Serializer):
'from_email': getattr(settings, 'DEFAULT_FROM_EMAIL'),
'request': request,
}
+
+ opts.update(self.get_email_options())
self.reset_form.save(**opts)
diff --git a/rest_auth/social_serializers.py b/rest_auth/social_serializers.py
new file mode 100644
index 0000000..087e6e1
--- /dev/null
+++ b/rest_auth/social_serializers.py
@@ -0,0 +1,78 @@
+from django.http import HttpRequest
+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
+try:
+ from allauth.socialaccount.helpers import complete_social_login
+except ImportError:
+ pass
+
+from allauth.socialaccount.models import SocialToken
+
+
+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()
+ app = adapter.get_provider().get_app(request)
+
+ if('access_token' in attrs) and ('token_secret' in attrs):
+ access_token = attrs.get('access_token')
+ token_secret = attrs.get('token_secret')
+ else:
+ raise serializers.ValidationError('Incorrect input. access_token and token_secret are required.')
+
+ 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 HTTPError:
+ raise serializers.ValidationError('Incorrect value')
+
+ 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/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..aab38d3 100644
--- a/rest_auth/tests/settings.py
+++ b/rest_auth/tests/settings.py
@@ -45,6 +45,13 @@ TEMPLATE_CONTEXT_PROCESSORS = [
"allauth.socialaccount.context_processors.socialaccount",
]
+REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'rest_framework.authentication.SessionAuthentication',
+ 'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
+ )
+}
+
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
@@ -64,7 +71,9 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'rest_auth',
- 'rest_auth.registration'
+ 'rest_auth.registration',
+
+ 'rest_framework_jwt'
]
SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd"
diff --git a/rest_auth/tests/test_api.py b/rest_auth/tests/test_api.py
index 222b3a7..f75322c 100644
--- a/rest_auth/tests/test_api.py
+++ b/rest_auth/tests/test_api.py
@@ -91,6 +91,19 @@ class APITestCase1(TestCase, BaseAPITestCase):
# test empty payload
self.post(self.login_url, data={}, status_code=400)
+ @override_settings(REST_USE_JWT=True)
+ def test_login_jwt(self):
+ payload = {
+ "username": self.USERNAME,
+ "password": self.PASS
+ }
+ user = 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')
@@ -280,12 +293,15 @@ class APITestCase1(TestCase, BaseAPITestCase):
self.assertEqual(len(mail.outbox), mail_count + 1)
def test_password_reset_with_invalid_email(self):
+ """
+ Invalid email should not raise error, as this would leak users
+ """
get_user_model().objects.create_user(self.USERNAME, self.EMAIL, self.PASS)
# call password reset
mail_count = len(mail.outbox)
payload = {'email': 'nonexisting@email.com'}
- self.post(self.password_reset_url, data=payload, status_code=400)
+ self.post(self.password_reset_url, data=payload, status_code=200)
self.assertEqual(len(mail.outbox), mail_count)
def test_user_details(self):
@@ -304,20 +320,52 @@ 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()
# test empty payload
self.post(self.register_url, data={}, status_code=400)
- self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201)
+ result = self.post(self.register_url, data=self.REGISTRATION_DATA, status_code=201)
+ self.assertIn('key', result.data)
self.assertEqual(get_user_model().objects.all().count(), user_count + 1)
+
new_user = get_user_model().objects.latest('id')
self.assertEqual(new_user.username, self.REGISTRATION_DATA['username'])
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'
@@ -339,11 +387,12 @@ class APITestCase1(TestCase, BaseAPITestCase):
status_code=status.HTTP_400_BAD_REQUEST
)
- self.post(
+ result = self.post(
self.register_url,
data=self.REGISTRATION_DATA_WITH_EMAIL,
status_code=status.HTTP_201_CREATED
)
+ self.assertNotIn('key', result.data)
self.assertEqual(get_user_model().objects.all().count(), user_count + 1)
self.assertEqual(len(mail.outbox), mail_count + 1)
new_user = get_user_model().objects.latest('id')
@@ -372,3 +421,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
+ 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
+ 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..97dfa31 100644
--- a/rest_auth/tests/test_base.py
+++ b/rest_auth/tests/test_base.py
@@ -37,7 +37,10 @@ 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(
diff --git a/rest_auth/tests/test_social.py b/rest_auth/tests/test_social.py
index 19509ef..34c912e 100644
--- a/rest_auth/tests/test_social.py
+++ b/rest_auth/tests/test_social.py
@@ -125,3 +125,29 @@ 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/utils.py b/rest_auth/utils.py
index a32da60..5cb94d7 100644
--- a/rest_auth/utils.py
+++ b/rest_auth/utils.py
@@ -9,3 +9,20 @@ def import_callable(path_or_callable):
assert isinstance(path_or_callable, string_types)
package, attr = path_or_callable.rsplit('.', 1)
return getattr(import_module(package), attr)
+
+
+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 52718eb..6946ef2 100644
--- a/rest_auth/views.py
+++ b/rest_auth/views.py
@@ -1,20 +1,25 @@
from django.contrib.auth import login, 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
from rest_framework.response import Response
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated, AllowAny
-from rest_framework.authtoken.models import Token
from rest_framework.generics import RetrieveUpdateAPIView
+from allauth.account import app_settings as allauth_settings
+
from .app_settings import (
TokenSerializer, UserDetailsSerializer, LoginSerializer,
PasswordResetSerializer, PasswordResetConfirmSerializer,
- PasswordChangeSerializer
+ PasswordChangeSerializer, JWTSerializer, create_token
)
+from .models import TokenModel
+
+from .utils import jwt_encode
class LoginView(GenericAPIView):
@@ -30,20 +35,40 @@ class LoginView(GenericAPIView):
"""
permission_classes = (AllowAny,)
serializer_class = LoginSerializer
- token_model = Token
- response_serializer = TokenSerializer
+ token_model = TokenModel
+
+ 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, created = self.token_model.objects.get_or_create(
- user=self.user)
+
+ 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)
+
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)
+ else:
+ serializer = serializer_class(instance=self.token)
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, *args, **kwargs):
self.serializer = self.get_serializer(data=self.request.data)
@@ -62,7 +87,23 @@ 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)
+ self.response = self.finalize_response(request, response, *args, **kwargs)
+ return self.response
+
def post(self, request):
+ return self.logout(request)
+
+ def logout(self, request):
try:
request.user.auth_token.delete()
except (AttributeError, ObjectDoesNotExist):
@@ -70,12 +111,11 @@ class LogoutView(APIView):
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.
@@ -112,13 +152,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.
@@ -134,11 +173,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.
@@ -171,4 +209,4 @@ class PasswordChangeView(GenericAPIView):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
- return Response({"success": "New Email has been saved."})
+ return Response({"success": _("New password has been saved.")})
diff --git a/setup.py b/setup.py
index d85b2d2..9b33b7f 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.0',
author='Sumit Chachra',
author_email='chachra@tivix.com',
url='http://github.com/Tivix/django-rest-auth',