Merge branch 'master' into developDemo

Conflicts:
	rest_auth/app_settings.py
	rest_auth/serializers.py
	rest_auth/views.py
 resolved conflicts
This commit is contained in:
Tabatha Memmott 2016-02-29 12:28:06 -08:00
commit e01fdc643a
28 changed files with 566 additions and 64 deletions

View File

@ -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

View File

@ -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'),

View File

@ -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

View File

@ -41,6 +41,7 @@
<li class="divider"></li>
<!-- these pages require user token -->
<li><a href="{% url 'user-details' %}">User details</a></li>
<li><a href="{% url 'logout' %}">Logout</a></li>
<li><a href="{% url 'password-change' %}">Password change</a></li>
</ul>
</li>

View File

@ -0,0 +1,20 @@
{% block content %}
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_logout' %}">{% csrf_token %}
<div class="form-group">
<label for="token" class="col-sm-2 control-label">User Token</label>
<div class="col-sm-4">
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
<p class="help-block">Token received after login</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Login</button>
</div>
</div>
<div class="form-group api-response"></div>
</form>
{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Logout</h3><hr/>
{% include "fragments/logout_form.html" %}
</div>
{% endblock %}

View File

@ -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 </installation>`
- access_token
- code
- /rest-auth/twitter/ (POST)
- access_token
- token_secret

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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."

View File

@ -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))

View File

@ -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):

View File

@ -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<key>\w+)/$', TemplateView.as_view(),
name='account_confirm_email'),
url(r'^change-email/$', VerifyEmailView.as_view(), name='rest_email_change'),

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -1,3 +1,4 @@
django-allauth>=0.19.1
responses>=0.3.0
flake8==2.4.0
djangorestframework-jwt>=1.7.2

View File

@ -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"

View File

@ -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)

View File

@ -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(

View File

@ -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)

View File

@ -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)

View File

@ -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.")})

View File

@ -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',