This commit is contained in:
Daniel Stanton 2017-01-26 19:11:08 +00:00 committed by GitHub
commit c5bc4255d7
15 changed files with 354 additions and 59 deletions

5
.gitignore vendored
View File

@ -55,3 +55,8 @@ db.sqlite3
# IntelliJ IDE files
.idea
# Virtual Environments
.vagrant/
cookbooks/
tmp/

View File

@ -4,77 +4,139 @@ API endpoints
Basic
-----
- /rest-auth/login/ (POST)
Typically, auth data is sent in the body, with a header ``Content-Type: application/x-www-form-urlencoded``
- username
- email
- password
/rest-auth/login/ (POST)
************************
**Request (standalone):**
Returns Token key
- ``username`` or ``email`` (email will be used to lookup username)
- ``password`` (required)
- /rest-auth/logout/ (POST)
**Request (using django-allauth):**
.. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout
- ``username`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'username'`` or ``'username_email'``)
- ``email`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'email'`` or ``'username_email'``)
- ``password`` (required)
- /rest-auth/password/reset/ (POST)
**Response:**
- email
- ``token``
- ``user`` (when using django-rest-framework-jwt or django-rest-knox)
- /rest-auth/password/reset/confirm/ (POST)
/rest-auth/logout/ (POST)
*************************
**Request (standalone):**
- uid
- token
- new_password1
- new_password2
- No values expected
.. note:: uid and token are sent in email after calling /rest-auth/password/reset/
**Request (using django-rest-knox):**
- /rest-auth/password/change/ (POST)
- ``Authorization: Token TOKEN`` *(Header)*
- new_password1
- new_password2
- old_password
**Response:**
.. note:: ``OLD_PASSWORD_FIELD_ENABLED = True`` to use old_password.
.. note:: ``LOGOUT_ON_PASSWORD_CHANGE = False`` to keep the user logged in after password change
- No values
- /rest-auth/user/ (GET, PUT, PATCH)
.. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout
- username
- first_name
- last_name
/rest-auth/logoutall/ (POST)
****************************
Returns pk, username, email, first_name, last_name
This endpoint deletes all Knox tokens, and will only be loaded when `REST_USE_KNOX = True`.
| **Request (using django-rest-knox):**
- `Authorization`: `Token TOKEN` (Header)
**Response:**
- No values
.. note:: ``ACCOUNT_LOGOUT_ON_GET = True`` to allow logout using GET - this is the exact same configuration from allauth. NOT recommended, see: http://django-allauth.readthedocs.io/en/latest/views.html#logout
/rest-auth/password/reset/ (POST)
*********************************
- email
/rest-auth/password/reset/confirm/ (POST)
*****************************************
- uid
- token
- new_password1
- new_password2
.. note:: uid and token are sent in email after calling /rest-auth/password/reset/
/rest-auth/password/change/ (POST)
**********************************
- new_password1
- new_password2
- old_password
.. note:: ``OLD_PASSWORD_FIELD_ENABLED = True`` to use old_password.
.. note:: ``LOGOUT_ON_PASSWORD_CHANGE = False`` to keep the user logged in after password change
/rest-auth/user/ (GET, PUT, PATCH)
**********************************
- username
- first_name
- last_name
Returns pk, username, email, first_name, last_name
Registration
------------
- /rest-auth/registration/ (POST)
/rest-auth/registration/ (POST)
*******************************
- username
- password1
- password2
- email
**Request (using django-allauth):**
- /rest-auth/registration/verify-email/ (POST)
- ``username`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'username'`` or ``'username_email'``, or when ``ACCOUNT_USERNAME_REQUIRED = True``)
- ``email`` (required when ``ACCOUNT_AUTHENTICATION_METHOD = 'email'`` or ``'username_email'``, or when ``ACCOUNT_EMAIL_REQUIRED = True``)
- ``password1`` (required)
- ``password2`` (required)
- key
**Response (using django-allauth):**
- No values
**Response (using django-allauth and django-rest-knox)**
- ``token``
- ``user``
/rest-auth/registration/verify-email/ (POST)
********************************************
**Request (using django-allauth):**
- ``key``
**Response (using django-allauth):**
- No values
Social Media Authentication
---------------------------
Basing on example from installation section :doc:`Installation </installation>`
Based on the example from the installation section :doc:`Installation </installation>`
- /rest-auth/facebook/ (POST)
/rest-auth/facebook/ (POST)
***************************
- access_token
- code
- ``access_token``
- ``code``
.. note:: ``access_token`` OR ``code`` can be used as standalone arguments, see https://github.com/Tivix/django-rest-auth/blob/master/rest_auth/registration/views.py
- /rest-auth/twitter/ (POST)
/rest-auth/twitter/ (POST)
**************************
- access_token
- token_secret
- ``access_token``
- ``token_secret``

View File

@ -12,6 +12,8 @@ Configuration
- JWT_SERIALIZER - (Using REST_USE_JWT=True) response for successful authentication in ``rest_auth.views.LoginView``, default value ``rest_auth.serializers.JWTSerializer``
- KNOX_SERIALIZER - (Using REST_USE_KNOX=True) response for successful authentication in ``rest_auth.views.LoginView`` and successful registration in ``rest_auth.registration.views.RegisterView`` (using ``django-allauth`` and ``ACCOUNT_EMAIL_VERIFICATION = 'optional' or 'none'``), default value ``rest_auth.serializers.KnoxSerializer``
- 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``
@ -48,6 +50,8 @@ Configuration
- **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)
- **REST_USE_KNOX** - Use Knox token authentication instead of the built-in Django-Rest-Framework token authentication. Knox makes some significant security improvements, and supports multiple tokens per user. https://github.com/James1345/django-rest-knox/ must be installed. Not compatible with ``REST_USE_JWT`` (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 (default: False)

View File

@ -153,7 +153,7 @@ 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.
1. Install ``django-rest-framework-jwt`` http://getblimp.github.io/django-rest-framework-jwt/ .
2. Add the following to your settings
@ -161,3 +161,19 @@ By default, ``django-rest-auth`` uses Django's Token-based authentication. If yo
REST_USE_JWT = True
Knox (optional)
---------------
By default, ``django-rest-auth`` uses Django's Token-based authentication. ``django-rest-knox`` provides more secure token authentication with additional features, including multiple tokens per user.
Knox and JWT cannot currently be used simultaneously.
1. Install ``django-rest-knox`` https://james1345.github.io/django-rest-knox/installation/ .
2. Configure ``django-rest-knox`` https://james1345.github.io/django-rest-knox/settings/ . ``REST_KNOX['USER_SERIALIZER']`` will not be used.
3. Add the following to your settings
.. code-block:: python
REST_USE_KNOX = True

View File

@ -2,6 +2,7 @@ from django.conf import settings
from rest_auth.serializers import (
TokenSerializer as DefaultTokenSerializer,
KnoxSerializer as DefaultKnoxSerializer,
JWTSerializer as DefaultJWTSerializer,
UserDetailsSerializer as DefaultUserDetailsSerializer,
LoginSerializer as DefaultLoginSerializer,
@ -21,6 +22,9 @@ TokenSerializer = import_callable(
JWTSerializer = import_callable(
serializers.get('JWT_SERIALIZER', DefaultJWTSerializer))
KnoxSerializer = import_callable(
serializers.get('KNOX_TOKEN_SERIALIZER', DefaultKnoxSerializer))
UserDetailsSerializer = import_callable(
serializers.get('USER_DETAILS_SERIALIZER', DefaultUserDetailsSerializer)
)

View File

@ -16,11 +16,12 @@ from allauth.account import app_settings as allauth_settings
from rest_auth.app_settings import (TokenSerializer,
JWTSerializer,
KnoxSerializer,
create_token)
from rest_auth.models import TokenModel
from rest_auth.registration.serializers import (SocialLoginSerializer,
VerifyEmailSerializer)
from rest_auth.utils import jwt_encode
from rest_auth.utils import create_knox_token, jwt_encode
from rest_auth.views import LoginView
from .app_settings import RegisterSerializer
@ -49,8 +50,14 @@ class RegisterView(CreateAPIView):
'token': self.token
}
return JWTSerializer(data).data
elif getattr(settings, 'REST_USE_KNOX', False):
data = {
'user': user,
'token': self.token
}
return KnoxSerializer(data).data
else:
return TokenSerializer(user.auth_token).data
return TokenSerializer(self.token).data
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
@ -65,9 +72,11 @@ class RegisterView(CreateAPIView):
def perform_create(self, serializer):
user = serializer.save(self.request)
if getattr(settings, 'REST_USE_JWT', False):
self.token = jwt_encode(user)
self.token = jwt_encode(user)
elif getattr(settings, 'REST_USE_KNOX', False):
self.token = create_knox_token(user)
else:
create_token(self.token_model, user, serializer)
self.token = create_token(self.token_model, user, serializer)
complete_signup(self.request._request, user,
allauth_settings.EMAIL_VERIFICATION,

View File

@ -114,7 +114,6 @@ class TokenSerializer(serializers.ModelSerializer):
"""
Serializer for Token model.
"""
class Meta:
model = TokenModel
fields = ('key',)
@ -133,7 +132,7 @@ class UserDetailsSerializer(serializers.ModelSerializer):
# Required to allow using custom USER_DETAILS_SERIALIZER in
# JWTSerializer. Defining it here to avoid circular imports
rest_auth_serializers = getattr(settings, 'REST_AUTH_SERIALIZERS', {})
JWTUserDetailsSerializer = import_callable(
CustomUserDetailsSerializer = import_callable(
rest_auth_serializers.get('USER_DETAILS_SERIALIZER', UserDetailsSerializer)
)
@ -143,7 +142,15 @@ class JWTSerializer(serializers.Serializer):
Serializer for JWT authentication.
"""
token = serializers.CharField()
user = JWTUserDetailsSerializer()
user = CustomUserDetailsSerializer()
class KnoxSerializer(serializers.Serializer):
"""
Serializer for Knox authentication.
"""
token = serializers.CharField()
user = CustomUserDetailsSerializer()
class PasswordResetSerializer(serializers.Serializer):

View File

@ -2,3 +2,4 @@ django-allauth>=0.19.1
responses>=0.3.0
flake8==2.4.0
djangorestframework-jwt>=1.7.2
django-rest-knox>=2.3.0

View File

@ -66,6 +66,15 @@ REST_FRAMEWORK = {
)
}
REST_FRAMEWORK_KNOX = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'knox.auth.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
@ -88,7 +97,9 @@ INSTALLED_APPS = [
'rest_auth',
'rest_auth.registration',
'rest_framework_jwt'
'rest_framework_jwt',
'knox'
]
SECRET_KEY = "38dh*skf8sjfhs287dh&^hd8&3hdg*j2&sd"

View File

@ -1,13 +1,23 @@
from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings
from django.contrib.auth import get_user_model
from django.core import mail
from django.conf import settings
from django.utils.encoding import force_text
from django.utils.six.moves import reload_module
from rest_framework import status
from rest_framework.test import APIClient
from rest_framework import views as drf_views
from unittest.mock import patch
from allauth.account import app_settings as account_app_settings
from knox.models import AuthToken
from .test_base import BaseAPITestCase
from .settings import REST_FRAMEWORK_KNOX
@override_settings(ROOT_URLCONF="tests.urls")
@ -148,6 +158,24 @@ class APITestCase1(TestCase, BaseAPITestCase):
self.assertEqual('token' in self.response.json.keys(), True)
self.token = self.response.json['token']
@override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},)
def test_login_knox(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']
self.assertEqual('user' in self.response.json.keys(), True)
self.user = self.response.json['user']
self.post(self.login_url, data=payload, status_code=200)
self.assertNotEqual(self.token, self.response.json['token'])
def test_login_by_email(self):
# starting test without allauth app
settings.INSTALLED_APPS.remove('allauth')
@ -382,6 +410,21 @@ class APITestCase1(TestCase, BaseAPITestCase):
user = get_user_model().objects.get(pk=user.pk)
self.assertEqual(user.email, self.response.json['email'])
@override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},)
def test_user_details_using_knox(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()
@ -411,6 +454,21 @@ class APITestCase1(TestCase, BaseAPITestCase):
self._login()
self._logout()
@override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},)
def test_registration_with_knox(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.assertIn('user', 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'
@ -493,3 +551,43 @@ class APITestCase1(TestCase, BaseAPITestCase):
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)
# @override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = {
# 'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},)
def test_logout_knox(self):
with override_settings(REST_USE_KNOX=True, REST_FRAMEWORK=REST_FRAMEWORK_KNOX):
reload_module(drf_views)
payload = {
"username": self.USERNAME,
"password": self.PASS
}
get_user_model().objects.create_user(self.USERNAME, '', self.PASS)
self.client = APIClient()
response = self.client.post(self.login_url, data=payload, status_code=status.HTTP_200_OK)
self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % response.data['token']))
self.client.post(self.logout_url, status_code=status.HTTP_200_OK)
self.assertEqual(AuthToken.objects.count(), 0)
reload_module(drf_views)
@override_settings(REST_USE_KNOX=True, REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication')},)
def test_logout_all_knox(self):
payload = {
"username": self.USERNAME,
"password": self.PASS
}
get_user_model().objects.create_user(self.USERNAME, '', self.PASS)
self.logout_all_url = reverse('rest_logout_all')
self.client = APIClient()
self.client.post(self.login_url, data=payload, status_code=status.HTTP_200_OK)
response = self.client.post(self.login_url, data=payload, status_code=status.HTTP_200_OK)
self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % response.data['token']))
self.client.post(self.logout_all_url, status_code=status.HTTP_200_OK)
self.assertEqual(AuthToken.objects.count(), 0)

View File

@ -8,6 +8,7 @@ from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
from rest_framework.decorators import api_view
from rest_auth.urls import urlpatterns
from rest_auth.views import LogoutAllView
from rest_auth.registration.views import SocialLoginView
from rest_auth.social_serializers import TwitterLoginSerializer
@ -49,5 +50,6 @@ urlpatterns += [
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')),
url(r'^logoutall/$', LogoutAllView.as_view(), name='rest_logout_all'),
]

View File

@ -1,8 +1,9 @@
from django.conf import settings
from django.conf.urls import url
from rest_auth.views import (
LoginView, LogoutView, UserDetailsView, PasswordChangeView,
PasswordResetView, PasswordResetConfirmView
LoginView, LogoutView, LogoutAllView, UserDetailsView,
PasswordChangeView, PasswordResetView, PasswordResetConfirmView
)
urlpatterns = [
@ -18,3 +19,8 @@ urlpatterns = [
url(r'^password/change/$', PasswordChangeView.as_view(),
name='rest_password_change'),
]
if getattr(settings, 'REST_USE_KNOX', False):
urlpatterns.append(
url(r'^logoutall/$', LogoutAllView.as_view(), name='rest_logout_all')
)

View File

@ -16,6 +16,15 @@ def default_create_token(token_model, user, serializer):
return token
def create_knox_token(user):
try:
from knox.models import AuthToken
except ImportError:
raise ImportError("django-rest-knox needs to be installed")
token = AuthToken.objects.create(user=user)
return token
def jwt_encode(user):
try:
from rest_framework_jwt.settings import api_settings

View File

@ -16,12 +16,19 @@ from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView
from rest_framework.permissions import IsAuthenticated, AllowAny
from .app_settings import (
TokenSerializer, UserDetailsSerializer, LoginSerializer,
PasswordResetSerializer, PasswordResetConfirmSerializer,
PasswordChangeSerializer, JWTSerializer, create_token
TokenSerializer, KnoxSerializer, UserDetailsSerializer,
LoginSerializer, PasswordResetSerializer,
PasswordResetConfirmSerializer, PasswordChangeSerializer,
JWTSerializer, create_token
)
from .models import TokenModel
from .utils import jwt_encode
from .utils import create_knox_token, jwt_encode
if getattr(settings, 'REST_USE_KNOX', False):
try:
from knox.auth import TokenAuthentication as KnoxTokenAuthentication
except ImportError:
raise ImportError("Install django-rest-knox to use REST_USE_KNOX = True")
sensitive_post_parameters_m = method_decorator(
sensitive_post_parameters(
@ -54,6 +61,8 @@ class LoginView(GenericAPIView):
def get_response_serializer(self):
if getattr(settings, 'REST_USE_JWT', False):
response_serializer = JWTSerializer
elif getattr(settings, 'REST_USE_KNOX', False):
response_serializer = KnoxSerializer
else:
response_serializer = TokenSerializer
return response_serializer
@ -63,6 +72,8 @@ class LoginView(GenericAPIView):
if getattr(settings, 'REST_USE_JWT', False):
self.token = jwt_encode(self.user)
elif getattr(settings, 'REST_USE_KNOX', False):
self.token = create_knox_token(self.user)
else:
self.token = create_token(self.token_model, self.user,
self.serializer)
@ -80,6 +91,13 @@ class LoginView(GenericAPIView):
}
serializer = serializer_class(instance=data,
context={'request': self.request})
elif getattr(settings, 'REST_USE_KNOX', 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})
@ -102,7 +120,11 @@ class LogoutView(APIView):
Accepts/Returns nothing.
"""
permission_classes = (AllowAny,)
if getattr(settings, 'REST_USE_KNOX', False):
authentication_classes = (KnoxTokenAuthentication,)
permission_classes = (IsAuthenticated,)
else:
permission_classes = (AllowAny,)
def get(self, request, *args, **kwargs):
if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', False):
@ -117,7 +139,44 @@ class LogoutView(APIView):
def logout(self, request):
try:
request.user.auth_token.delete()
if getattr(settings, 'REST_USE_KNOX', False):
request._auth.delete()
else:
request.user.auth_token.delete()
except (AttributeError, ObjectDoesNotExist):
pass
django_logout(request)
return Response({"detail": _("Successfully logged out.")},
status=status.HTTP_200_OK)
class LogoutAllView(APIView):
"""
Calls Django logout method and deletes all the Knox tokens
assigned to the current User object.
Accepts/Returns nothing.
"""
if getattr(settings, 'REST_USE_KNOX', False):
authentication_classes = (KnoxTokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
if getattr(settings, 'ACCOUNT_LOGOUT_ON_GET', False):
response = self.logout_all(request)
else:
response = self.http_method_not_allowed(request, *args, **kwargs)
return self.finalize_response(request, response, *args, **kwargs)
def post(self, request):
return self.logout_all(request)
def logout_all(self, request):
try:
request.user.auth_token_set.all().delete()
except (AttributeError, ObjectDoesNotExist):
pass

View File

@ -38,6 +38,8 @@ setup(
tests_require=[
'responses>=0.5.0',
'django-allauth>=0.25.0',
'djangorestframework-jwt>=1.9.0',
'django-rest-knox>=2.3.0'
],
test_suite='runtests.runtests',
include_package_data=True,