Merge pull request #5 from Tivix/master

Pull updates
This commit is contained in:
Egor 2016-08-01 09:00:38 +04:00 committed by GitHub
commit 5086eebfdb
31 changed files with 853 additions and 158 deletions

26
.coveragerc Normal file
View File

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

3
.gitignore vendored
View File

@ -35,9 +35,6 @@ nosetests.xml
coverage.xml coverage.xml
coverage_html coverage_html
# Translations
*.mo
# Mr Developer # Mr Developer
.mr.developer.cfg .mr.developer.cfg
.project .project

View File

@ -1,13 +1,18 @@
language: python language: python
python: python:
- "2.7" - "2.7"
- "3.5"
env: env:
- DJANGO=1.7.7 - DJANGO=1.8.13
- DJANGO=1.8 - DJANGO=1.9.7
install: install:
- pip install -q Django==$DJANGO --use-mirrors - pip install -q Django==$DJANGO --use-mirrors
- pip install coveralls - pip install coveralls
- pip install -r rest_auth/tests/requirements.pip - pip install -r rest_auth/tests/requirements.pip
matrix:
exclude:
- python: "3.5"
env: DJANGO=1.8.13
script: script:
- coverage run --source=rest_auth setup.py test - coverage run --source=rest_auth setup.py test
after_success: after_success:

View File

@ -11,6 +11,8 @@ urlpatterns = [
name='email-verification'), name='email-verification'),
url(r'^login/$', TemplateView.as_view(template_name="login.html"), url(r'^login/$', TemplateView.as_view(template_name="login.html"),
name='login'), name='login'),
url(r'^logout/$', TemplateView.as_view(template_name="logout.html"),
name='logout'),
url(r'^password-reset/$', url(r'^password-reset/$',
TemplateView.as_view(template_name="password_reset.html"), TemplateView.as_view(template_name="password_reset.html"),
name='password-reset'), name='password-reset'),

View File

@ -1,4 +1,4 @@
django>=1.7.0 django>=1.8.0
django-rest-auth==0.6.0 django-rest-auth==0.8.1
django-allauth==0.24.1 django-allauth>=0.24.1
six==1.9.0 six==1.9.0

View File

@ -40,6 +40,7 @@
<li class="divider"></li> <li class="divider"></li>
<!-- these pages require user token --> <!-- these pages require user token -->
<li><a href="{% url 'user-details' %}">User details</a></li> <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> <li><a href="{% url 'password-change' %}">Password change</a></li>
</ul> </ul>
</li> </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) - 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) - /rest-auth/password/reset/ (POST)
@ -70,3 +74,8 @@ Basing on example from installation section :doc:`Installation </installation>`
- access_token - access_token
- code - code
- /rest-auth/twitter/ (POST)
- access_token
- token_secret

View File

@ -1,6 +1,18 @@
Changelog 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 0.6.0
----- -----
- dropped support for Python 2.6 - 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`` - 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`` - 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`` - 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. You can define your custom serializers for registration endpoint.
Possible key values: 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`` - **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_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) - **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 - **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',
},
}

View File

@ -6,7 +6,7 @@
Welcome to django-rest-auth's documentation! 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 .. note:: django-rest-auth from v0.3.3 supports django-rest-framework v3.0

View File

@ -38,19 +38,24 @@ You're good to go now!
Registration (optional) 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 .. code-block:: python
INSTALLED_APPS = ( INSTALLED_APPS = (
..., ...,
'django.contrib.sites',
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'rest_auth.registration', 'rest_auth.registration',
) )
SITE_ID = 1
3. Add rest_auth.registration urls: 3. Add rest_auth.registration urls:
.. code-block:: python .. code-block:: python
@ -65,11 +70,11 @@ Registration (optional)
Social Authentication (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 .. code-block:: python
@ -79,16 +84,22 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati
'rest_framework.authtoken', 'rest_framework.authtoken',
'rest_auth' 'rest_auth'
..., ...,
'django.contrib.sites',
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'rest_auth.registration', 'rest_auth.registration',
..., ...,
'allauth.socialaccount', 'allauth.socialaccount',
'allauth.socialaccount.providers.facebook', 'allauth.socialaccount.providers.facebook',
'allauth.socialaccount.providers.twitter',
) )
2. Add Social Application in django admin panel 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: 3. Create new view as a subclass of ``rest_auth.registration.views.SocialLoginView`` with ``FacebookOAuth2Adapter`` adapter as an attribute:
.. code-block:: python .. code-block:: python
@ -103,9 +114,50 @@ Using ``django-allauth``, ``django-rest-auth`` provides helpful class for creati
.. code-block:: python .. code-block:: python
urlpatterns += pattern('', urlpatterns += patterns('',
..., ...,
url(r'^rest-auth/facebook/$', FacebookLogin.as_view(), name='fb_login') 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. .. 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 ( from rest_auth.serializers import (
TokenSerializer as DefaultTokenSerializer, TokenSerializer as DefaultTokenSerializer,
JWTSerializer as DefaultJWTSerializer,
UserDetailsSerializer as DefaultUserDetailsSerializer, UserDetailsSerializer as DefaultUserDetailsSerializer,
LoginSerializer as DefaultLoginSerializer, LoginSerializer as DefaultLoginSerializer,
PasswordResetSerializer as DefaultPasswordResetSerializer, PasswordResetSerializer as DefaultPasswordResetSerializer,
@ -17,6 +18,9 @@ serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {})
TokenSerializer = import_callable( TokenSerializer = import_callable(
serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer)) serializers.get('TOKEN_SERIALIZER', DefaultTokenSerializer))
JWTSerializer = import_callable(
serializers.get('JWT_SERIALIZER', DefaultJWTSerializer))
UserDetailsSerializer = import_callable( UserDetailsSerializer = import_callable(
serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer) serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer)
) )

Binary file not shown.

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,5 +1,6 @@
from django.http import HttpRequest from django.http import HttpRequest
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _
try: try:
from allauth.account import app_settings as allauth_settings from allauth.account import app_settings as allauth_settings
@ -14,13 +15,8 @@ from rest_framework import serializers
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
# Import is needed only if we are using social login, in which # Import is needed only if we are using social login, in which
# case the allauth.socialaccount will be declared # case the allauth.socialaccount will be declared
if 'allauth.socialaccount' in settings.INSTALLED_APPS: if 'allauth.socialaccount' in settings.INSTALLED_APPS:
try:
from allauth.socialaccount.helpers import complete_social_login from allauth.socialaccount.helpers import complete_social_login
except ImportError:
pass
class SocialLoginSerializer(serializers.Serializer): class SocialLoginSerializer(serializers.Serializer):
@ -53,14 +49,14 @@ class SocialLoginSerializer(serializers.Serializer):
if not view: if not view:
raise serializers.ValidationError( 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) adapter_class = getattr(view, 'adapter_class', None)
if not adapter_class: 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) app = adapter.get_provider().get_app(request)
# More info on code vs access_token # More info on code vs access_token
@ -77,11 +73,11 @@ class SocialLoginSerializer(serializers.Serializer):
if not self.callback_url: if not self.callback_url:
raise serializers.ValidationError( raise serializers.ValidationError(
'Define callback_url in view' _('Define callback_url in view')
) )
if not self.client_class: if not self.client_class:
raise serializers.ValidationError( raise serializers.ValidationError(
'Define client_class in view' _('Define client_class in view')
) )
code = attrs.get('code') code = attrs.get('code')
@ -101,7 +97,7 @@ class SocialLoginSerializer(serializers.Serializer):
access_token = token['access_token'] access_token = token['access_token']
else: 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 = adapter.parse_token({'access_token': access_token})
token.app = app token.app = app
@ -110,7 +106,7 @@ class SocialLoginSerializer(serializers.Serializer):
login = self.get_social_login(adapter, app, token, access_token) login = self.get_social_login(adapter, app, token, access_token)
complete_social_login(request, login) complete_social_login(request, login)
except HTTPError: except HTTPError:
raise serializers.ValidationError('Incorrect value') raise serializers.ValidationError(_('Incorrect value'))
if not login.is_existing: if not login.is_existing:
login.lookup() login.lookup()
@ -139,7 +135,7 @@ class RegisterSerializer(serializers.Serializer):
if allauth_settings.UNIQUE_EMAIL: if allauth_settings.UNIQUE_EMAIL:
if email and email_address_exists(email): if email and email_address_exists(email):
raise serializers.ValidationError( 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 return email
def validate_password1(self, password): def validate_password1(self, password):
@ -147,7 +143,7 @@ class RegisterSerializer(serializers.Serializer):
def validate(self, data): def validate(self, data):
if data['password1'] != data['password2']: 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 return data
def custom_signup(self, request, user): def custom_signup(self, request, user):

View File

@ -17,7 +17,7 @@ urlpatterns = [
# with proper key. # with proper key.
# If you don't want to use API on that step, then just use ConfirmEmailView # If you don't want to use API on that step, then just use ConfirmEmailView
# view from: # 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(), url(r'^account-confirm-email/(?P<key>[-:\w]+)/$', TemplateView.as_view(),
name='account_confirm_email'), name='account_confirm_email'),
] ]

View File

@ -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.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from rest_framework import status 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.views import ConfirmEmailView
from allauth.account.utils import complete_signup from allauth.account.utils import complete_signup
from allauth.account import app_settings as allauth_settings 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) create_token)
from rest_auth.registration.serializers import (SocialLoginSerializer, from rest_auth.registration.serializers import (SocialLoginSerializer,
VerifyEmailSerializer) VerifyEmailSerializer)
@ -17,17 +21,27 @@ from rest_auth.views import LoginView
from rest_auth.models import TokenModel from rest_auth.models import TokenModel
from .app_settings import RegisterSerializer from .app_settings import RegisterSerializer
from rest_auth.utils import jwt_encode
class RegisterView(CreateAPIView): class RegisterView(CreateAPIView):
serializer_class = RegisterSerializer serializer_class = RegisterSerializer
permission_classes = (AllowAny, ) permission_classes = (AllowAny, )
token_model = TokenModel token_model = TokenModel
throttle_scope = 'register_view'
def get_response_data(self, user): def get_response_data(self, user):
if allauth_settings.EMAIL_VERIFICATION == \ if allauth_settings.EMAIL_VERIFICATION == \
allauth_settings.EmailVerificationMethod.MANDATORY: allauth_settings.EmailVerificationMethod.MANDATORY:
return {} 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 return TokenSerializer(user.auth_token).data
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -40,7 +54,11 @@ class RegisterView(CreateAPIView):
def perform_create(self, serializer): def perform_create(self, serializer):
user = serializer.save(self.request) user = serializer.save(self.request)
if getattr(settings, 'REST_USE_JWT', False):
self.token = jwt_encode(user)
else:
create_token(self.token_model, user, serializer) create_token(self.token_model, user, serializer)
complete_signup(self.request._request, user, complete_signup(self.request._request, user,
allauth_settings.EMAIL_VERIFICATION, allauth_settings.EMAIL_VERIFICATION,
None) None)
@ -52,16 +70,13 @@ class VerifyEmailView(APIView, ConfirmEmailView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
allowed_methods = ('POST', 'OPTIONS', 'HEAD') allowed_methods = ('POST', 'OPTIONS', 'HEAD')
def get(self, *args, **kwargs):
raise MethodNotAllowed('GET')
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
serializer = VerifyEmailSerializer(data=request.data) serializer = VerifyEmailSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
self.kwargs['key'] = serializer.validated_data['key'] self.kwargs['key'] = serializer.validated_data['key']
confirmation = self.get_object() confirmation = self.get_object()
confirmation.confirm(self.request) 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): class SocialLoginView(LoginView):
@ -89,3 +104,6 @@ class SocialLoginView(LoginView):
""" """
serializer_class = SocialLoginSerializer serializer_class = SocialLoginSerializer
def process_login(self):
get_adapter(self.request).login(self.request, self.user)

View File

@ -103,7 +103,7 @@ class LoginSerializer(serializers.Serializer):
if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY: if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:
email_address = user.emailaddress_set.get(email=user.email) email_address = user.emailaddress_set.get(email=user.email)
if not email_address.verified: if not email_address.verified:
raise serializers.ValidationError('E-mail is not verified.') raise serializers.ValidationError(_('E-mail is not verified.'))
attrs['user'] = user attrs['user'] = user
return attrs return attrs
@ -130,6 +130,14 @@ class UserDetailsSerializer(serializers.ModelSerializer):
read_only_fields = ('email', ) read_only_fields = ('email', )
class JWTSerializer(serializers.Serializer):
"""
Serializer for JWT authentication.
"""
token = serializers.CharField()
user = UserDetailsSerializer()
class PasswordResetSerializer(serializers.Serializer): class PasswordResetSerializer(serializers.Serializer):
""" """
@ -149,7 +157,7 @@ class PasswordResetSerializer(serializers.Serializer):
# Create PasswordResetForm with the serializer # Create PasswordResetForm with the serializer
self.reset_form = self.password_reset_form_class(data=self.initial_data) self.reset_form = self.password_reset_form_class(data=self.initial_data)
if not self.reset_form.is_valid(): if not self.reset_form.is_valid():
raise serializers.ValidationError(_('Error')) raise serializers.ValidationError(self.reset_form.errors)
return value return value

View File

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

View File

@ -1,79 +1,16 @@
# Moved in Django 1.8 from django to tests/auth_tests/urls.py # Moved in Django 1.8 from django to tests/auth_tests/urls.py
from django.conf.urls import include, url from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth import views from django.contrib.auth import views
from django.contrib.auth.decorators import login_required 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.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 # special urls for auth test cases
urlpatterns += [ urlpatterns += [
url(r'^logout/custom_query/$', views.logout, dict(redirect_field_name='follow')), 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/$', views.logout, dict(next_page='/somewhere/')),
url(r'^logout/next_page/named/$', views.logout, dict(next_page='password_reset')), 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_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/$', views.password_reset, dict(post_reset_redirect='/custom/')),
url(r'^password_reset/custom_redirect/named/$', views.password_reset, dict(post_reset_redirect='password_reset')), 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'^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_required(views.password_reset)),
url(r'^login_required_login_url/$', login_required(views.password_reset, login_url='/somewhere/')), 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)),
] ]

View File

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

View File

@ -45,6 +45,27 @@ TEMPLATE_CONTEXT_PROCESSORS = [
"allauth.socialaccount.context_processors.socialaccount", "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 = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@ -59,14 +80,24 @@ INSTALLED_APPS = [
'allauth.account', 'allauth.account',
'allauth.socialaccount', 'allauth.socialaccount',
'allauth.socialaccount.providers.facebook', 'allauth.socialaccount.providers.facebook',
'allauth.socialaccount.providers.twitter',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'rest_auth', 'rest_auth',
'rest_auth.registration' 'rest_auth.registration',
'rest_framework_jwt'
] ]
SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd" SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd"
ACCOUNT_ACTIVATION_DAYS = 1 ACCOUNT_ACTIVATION_DAYS = 1
SITE_ID = 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',
)

View File

@ -1,16 +1,16 @@
from django.core.urlresolvers import reverse 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.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.conf import settings from django.conf import settings
from django.test.utils import override_settings
from django.utils.encoding import force_text from django.utils.encoding import force_text
from rest_framework import status from rest_framework import status
from allauth.account import app_settings as account_app_settings
from .test_base import BaseAPITestCase from .test_base import BaseAPITestCase
@override_settings(ROOT_URLCONF="tests.urls")
class APITestCase1(TestCase, BaseAPITestCase): class APITestCase1(TestCase, BaseAPITestCase):
""" """
Case #1: Case #1:
@ -18,7 +18,7 @@ class APITestCase1(TestCase, BaseAPITestCase):
- custom registration: backend defined - custom registration: backend defined
""" """
urls = 'tests.urls' # urls = 'tests.urls'
USERNAME = 'person' USERNAME = 'person'
PASS = 'person' PASS = 'person'
@ -57,7 +57,36 @@ class APITestCase1(TestCase, BaseAPITestCase):
result['token'] = default_token_generator.make_token(user) result['token'] = default_token_generator.make_token(user)
return result 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 = { payload = {
"username": self.USERNAME, "username": self.USERNAME,
"password": self.PASS "password": self.PASS
@ -91,6 +120,34 @@ class APITestCase1(TestCase, BaseAPITestCase):
# test empty payload # test empty payload
self.post(self.login_url, data={}, status_code=400) 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): def test_login_by_email(self):
# starting test without allauth app # starting test without allauth app
settings.INSTALLED_APPS.remove('allauth') settings.INSTALLED_APPS.remove('allauth')
@ -136,6 +193,9 @@ class APITestCase1(TestCase, BaseAPITestCase):
# test empty payload # test empty payload
self.post(self.login_url, data={}, status_code=400) self.post(self.login_url, data={}, status_code=400)
# bring back allauth
settings.INSTALLED_APPS.append('allauth')
def test_password_change(self): def test_password_change(self):
login_payload = { login_payload = {
"username": self.USERNAME, "username": self.USERNAME,
@ -307,6 +367,21 @@ class APITestCase1(TestCase, BaseAPITestCase):
self.assertEqual(user.last_name, self.response.json['last_name']) self.assertEqual(user.last_name, self.response.json['last_name'])
self.assertEqual(user.email, self.response.json['email']) 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): def test_registration(self):
user_count = get_user_model().objects.all().count() user_count = get_user_model().objects.all().count()
@ -323,6 +398,19 @@ class APITestCase1(TestCase, BaseAPITestCase):
self._login() self._login()
self._logout() 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): def test_registration_with_invalid_password(self):
data = self.REGISTRATION_DATA.copy() data = self.REGISTRATION_DATA.copy()
data['password2'] = 'foobar' data['password2'] = 'foobar'
@ -331,7 +419,8 @@ class APITestCase1(TestCase, BaseAPITestCase):
@override_settings( @override_settings(
ACCOUNT_EMAIL_VERIFICATION='mandatory', ACCOUNT_EMAIL_VERIFICATION='mandatory',
ACCOUNT_EMAIL_REQUIRED=True ACCOUNT_EMAIL_REQUIRED=True,
ACCOUNT_EMAIL_CONFIRMATION_HMAC=False
) )
def test_registration_with_email_verification(self): def test_registration_with_email_verification(self):
user_count = get_user_model().objects.all().count() user_count = get_user_model().objects.all().count()
@ -378,3 +467,29 @@ class APITestCase1(TestCase, BaseAPITestCase):
# try to login again # try to login again
self._login() self._login()
self._logout() 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)

View File

@ -37,17 +37,22 @@ class BaseAPITestCase(object):
# check_headers = kwargs.pop('check_headers', True) # check_headers = kwargs.pop('check_headers', True)
if hasattr(self, 'token'): if hasattr(self, 'token'):
if getattr(settings, 'REST_USE_JWT', False):
kwargs['HTTP_AUTHORIZATION'] = 'JWT %s' % self.token
else:
kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token kwargs['HTTP_AUTHORIZATION'] = 'Token %s' % self.token
self.response = request_func(*args, **kwargs) self.response = request_func(*args, **kwargs)
is_json = bool( is_json = bool(
[x for x in self.response._headers['content-type'] if 'json' in x]) [x for x in self.response._headers['content-type'] if 'json' in x])
self.response.json = {}
if is_json and self.response.content: if is_json and self.response.content:
self.response.json = json.loads(force_text(self.response.content)) self.response.json = json.loads(force_text(self.response.content))
else:
self.response.json = {}
if status_code: if status_code:
self.assertEqual(self.response.status_code, status_code) self.assertEqual(self.response.status_code, status_code)
return self.response return self.response
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
@ -94,6 +99,9 @@ class BaseAPITestCase(object):
self.user_url = reverse('rest_user_details') self.user_url = reverse('rest_user_details')
self.veirfy_email_url = reverse('rest_verify_email') self.veirfy_email_url = reverse('rest_verify_email')
self.fb_login_url = reverse('fb_login') 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): def _login(self):
payload = { payload = {

View File

@ -1,3 +1,5 @@
import json
from django.test import TestCase from django.test import TestCase
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test.utils import override_settings from django.test.utils import override_settings
@ -12,10 +14,9 @@ from rest_framework import status
from .test_base import BaseAPITestCase from .test_base import BaseAPITestCase
@override_settings(ROOT_URLCONF="tests.urls")
class TestSocialAuth(TestCase, BaseAPITestCase): class TestSocialAuth(TestCase, BaseAPITestCase):
urls = 'tests.urls'
USERNAME = 'person' USERNAME = 'person'
PASS = 'person' PASS = 'person'
EMAIL = "person1@world.com" EMAIL = "person1@world.com"
@ -35,9 +36,19 @@ class TestSocialAuth(TestCase, BaseAPITestCase):
client_id='123123123', client_id='123123123',
secret='321321321', secret='321321321',
) )
twitter_social_app = SocialApp.objects.create(
provider='twitter',
name='Twitter',
client_id='11223344',
secret='55667788',
)
site = Site.objects.get_current() site = Site.objects.get_current()
social_app.sites.add(site) social_app.sites.add(site)
twitter_social_app.sites.add(site)
self.graph_api_url = GRAPH_API_URL + '/me' self.graph_api_url = GRAPH_API_URL + '/me'
self.twitter_url = 'http://twitter.com/foobarme'
@responses.activate @responses.activate
def test_failed_social_auth(self): def test_failed_social_auth(self):
@ -58,11 +69,24 @@ class TestSocialAuth(TestCase, BaseAPITestCase):
@responses.activate @responses.activate
def test_social_auth(self): def test_social_auth(self):
# fake response for facebook call # 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.add(
responses.GET, responses.GET,
self.graph_api_url, self.graph_api_url,
body=resp_body, body=json.dumps(resp_body),
status=200, status=200,
content_type='application/json' content_type='application/json'
) )
@ -81,18 +105,146 @@ class TestSocialAuth(TestCase, BaseAPITestCase):
self.assertIn('key', self.response.json.keys()) self.assertIn('key', self.response.json.keys())
self.assertEqual(get_user_model().objects.all().count(), users_count + 1) 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 @responses.activate
@override_settings( @override_settings(
ACCOUNT_EMAIL_VERIFICATION='mandatory', ACCOUNT_EMAIL_VERIFICATION='mandatory',
ACCOUNT_EMAIL_REQUIRED=True, ACCOUNT_EMAIL_REQUIRED=True,
REST_SESSION_LOGIN=False REST_SESSION_LOGIN=False,
ACCOUNT_EMAIL_CONFIRMATION_HMAC=False
) )
def test_edge_case(self): 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.add(
responses.GET, responses.GET,
self.graph_api_url, self.graph_api_url,
body=resp_body % self.EMAIL, body=json.dumps(resp_body),
status=200, status=200,
content_type='application/json' content_type='application/json'
) )
@ -125,3 +277,28 @@ class TestSocialAuth(TestCase, BaseAPITestCase):
self.post(self.fb_login_url, data=payload, status_code=200) self.post(self.fb_login_url, data=payload, status_code=200)
self.assertIn('key', self.response.json.keys()) 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

@ -3,21 +3,51 @@ from django.views.generic import TemplateView
from . import django_urls from . import django_urls
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter 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.urls import urlpatterns
from rest_auth.registration.views import SocialLoginView from rest_auth.registration.views import SocialLoginView
from rest_auth.social_serializers import TwitterLoginSerializer
class FacebookLogin(SocialLoginView): class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter 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 += [ urlpatterns += [
url(r'^rest-registration/', include('rest_auth.registration.urls')), url(r'^rest-registration/', include('rest_auth.registration.urls')),
url(r'^test-admin/', include(django_urls)), url(r'^test-admin/', include(django_urls)),
url(r'^account-email-verification-sent/$', TemplateView.as_view(), url(r'^account-email-verification-sent/$', TemplateView.as_view(),
name='account_email_verification_sent'), name='account_email_verification_sent'),
url(r'^account-confirm-email/(?P<key>\w+)/$', TemplateView.as_view(), url(r'^account-confirm-email/(?P<key>[-:\w]+)/$', TemplateView.as_view(),
name='account_confirm_email'), name='account_confirm_email'),
url(r'^social-login/facebook/$', FacebookLogin.as_view(), name='fb_login'), 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')) url(r'^accounts/', include('allauth.socialaccount.urls'))
] ]

View File

@ -14,3 +14,16 @@ def import_callable(path_or_callable):
def default_create_token(token_model, user, serializer): def default_create_token(token_model, user, serializer):
token, _ = token_model.objects.get_or_create(user=user) token, _ = token_model.objects.get_or_create(user=user)
return token 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,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.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
from rest_framework import status from rest_framework import status
from rest_framework.views import APIView 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.permissions import IsAuthenticated, AllowAny
from rest_framework.generics import RetrieveUpdateAPIView from rest_framework.generics import RetrieveUpdateAPIView
from allauth.account import app_settings as allauth_settings
from .app_settings import ( from .app_settings import (
TokenSerializer, UserDetailsSerializer, LoginSerializer, TokenSerializer, UserDetailsSerializer, LoginSerializer,
PasswordResetSerializer, PasswordResetConfirmSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer,
PasswordChangeSerializer, create_token PasswordChangeSerializer, JWTSerializer, create_token
) )
from .models import TokenModel from .models import TokenModel
from .utils import jwt_encode
class LoginView(GenericAPIView): class LoginView(GenericAPIView):
@ -31,22 +39,47 @@ class LoginView(GenericAPIView):
permission_classes = (AllowAny,) permission_classes = (AllowAny,)
serializer_class = LoginSerializer serializer_class = LoginSerializer
token_model = TokenModel token_model = TokenModel
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 response_serializer = TokenSerializer
return response_serializer
def login(self): def login(self):
self.user = self.serializer.validated_data['user'] self.user = self.serializer.validated_data['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) self.token = create_token(self.token_model, self.user, self.serializer)
if getattr(settings, 'REST_SESSION_LOGIN', True): if getattr(settings, 'REST_SESSION_LOGIN', True):
login(self.request, self.user) self.process_login()
def get_response(self): def get_response(self):
return Response( serializer_class = self.get_response_serializer()
self.response_serializer(self.token).data, status=status.HTTP_200_OK
) 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): def post(self, request, *args, **kwargs):
self.request = request
self.serializer = self.get_serializer(data=self.request.data) self.serializer = self.get_serializer(data=self.request.data)
self.serializer.is_valid(raise_exception=True) self.serializer.is_valid(raise_exception=True)
self.login() self.login()
return self.get_response() return self.get_response()
@ -61,20 +94,33 @@ class LogoutView(APIView):
""" """
permission_classes = (AllowAny,) 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): def post(self, request):
return self.logout(request)
def logout(self, request):
try: try:
request.user.auth_token.delete() request.user.auth_token.delete()
except (AttributeError, ObjectDoesNotExist): except (AttributeError, ObjectDoesNotExist):
pass pass
logout(request) django_logout(request)
return Response({"success": "Successfully logged out."}, return Response({"success": _("Successfully logged out.")},
status=status.HTTP_200_OK) status=status.HTTP_200_OK)
class UserDetailsView(RetrieveUpdateAPIView): class UserDetailsView(RetrieveUpdateAPIView):
""" """
Returns User's details in JSON format. Returns User's details in JSON format.
@ -111,13 +157,12 @@ class PasswordResetView(GenericAPIView):
serializer.save() serializer.save()
# Return the success message with OK HTTP status # Return the success message with OK HTTP status
return Response( return Response(
{"success": "Password reset e-mail has been sent."}, {"success": _("Password reset e-mail has been sent.")},
status=status.HTTP_200_OK status=status.HTTP_200_OK
) )
class PasswordResetConfirmView(GenericAPIView): class PasswordResetConfirmView(GenericAPIView):
""" """
Password reset e-mail link is confirmed, therefore this resets the user's password. 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 = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() 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): class PasswordChangeView(GenericAPIView):
""" """
Calls Django Auth SetPasswordForm save method. Calls Django Auth SetPasswordForm save method.
@ -152,4 +196,4 @@ class PasswordChangeView(GenericAPIView):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
serializer.save() serializer.save()
return Response({"success": "New password has been saved."}) return Response({"success": _("New password has been saved.")})

View File

@ -18,7 +18,7 @@ f.close()
setup( setup(
name='django-rest-auth', name='django-rest-auth',
version='0.6.0', version='0.8.1',
author='Sumit Chachra', author='Sumit Chachra',
author_email='chachra@tivix.com', author_email='chachra@tivix.com',
url='http://github.com/Tivix/django-rest-auth', url='http://github.com/Tivix/django-rest-auth',
@ -28,16 +28,16 @@ setup(
keywords='django rest auth registration rest-framework django-registration api', keywords='django rest auth registration rest-framework django-registration api',
zip_safe=False, zip_safe=False,
install_requires=[ install_requires=[
'Django>=1.7.0', 'Django>=1.8.0',
'djangorestframework>=3.1.0', 'djangorestframework>=3.1.0',
'six>=1.9.0', 'six>=1.9.0',
], ],
extras_require={ extras_require={
'with_social': ['django-allauth>=0.24.1'], 'with_social': ['django-allauth>=0.25.0'],
}, },
tests_require=[ tests_require=[
'responses>=0.5.0', 'responses>=0.5.0',
'django-allauth>=0.24.1', 'django-allauth>=0.25.0',
], ],
test_suite='runtests.runtests', test_suite='runtests.runtests',
include_package_data=True, include_package_data=True,