version 0.0.7: new model, added endpoint ot logout, refactored auth views and serializers

This commit is contained in:
Anatoly 2019-08-14 11:27:48 +03:00
parent 68f6e0bc9c
commit e830e30c90
8 changed files with 189 additions and 80 deletions

View File

@ -0,0 +1,29 @@
# Generated by Django 2.2.4 on 2019-08-13 16:05
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authorization', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='BlacklistedAccessToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('jti', models.CharField(max_length=255, unique=True, verbose_name='Unique access_token identifier')),
('token', models.TextField(verbose_name='Access token')),
('blacklisted_at', models.DateTimeField(auto_now_add=True, verbose_name='Blacklisted datetime')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'unique_together': {('token', 'user')},
},
),
]

View File

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from oauth2_provider import models as oauth2_models from oauth2_provider import models as oauth2_models
from oauth2_provider.models import AbstractApplication from oauth2_provider.models import AbstractApplication
from django.utils.translation import gettext_lazy as _
from utils.models import PlatformMixin from utils.models import PlatformMixin
@ -30,3 +31,42 @@ class Application(PlatformMixin, AbstractApplication):
def natural_key(self): def natural_key(self):
return (self.client_id,) return (self.client_id,)
class BlacklistedAccessTokenQuerySet(models.QuerySet):
"""Queryset for model BlacklistedAccessToken"""
def by_user(self, user):
"""Filter by user"""
return self.filter(user=user)
def by_token(self, token):
"""Filter by token"""
return self.filter(token=token)
def by_jti(self, jti):
"""Filter by unique access_token identifier"""
return self.filter(jti=jti)
class BlacklistedAccessToken(models.Model):
user = models.ForeignKey('account.User',
on_delete=models.CASCADE,
verbose_name=_('User'))
jti = models.CharField(max_length=255, unique=True,
verbose_name=_('Unique access_token identifier'))
token = models.TextField(verbose_name=_('Access token'))
blacklisted_at = models.DateTimeField(auto_now_add=True,
verbose_name=_('Blacklisted datetime'))
objects = BlacklistedAccessTokenQuerySet.as_manager()
class Meta:
"""Meta class"""
unique_together = ('token', 'user')
def __str__(self):
return 'Blacklisted access token for {}'.format(self.user)

View File

@ -6,11 +6,12 @@ from django.contrib.auth import authenticate
from django.conf import settings from django.conf import settings
from account import models as account_models from account import models as account_models
from authorization.models import Application from authorization.models import Application, BlacklistedAccessToken
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from rest_framework_simplejwt.tokens import RefreshToken
# JWT # JWT
from rest_framework_simplejwt.tokens import RefreshToken, SlidingToken, UntypedToken from rest_framework_simplejwt import tokens
JWT_SETTINGS = settings.SIMPLE_JWT JWT_SETTINGS = settings.SIMPLE_JWT
@ -34,7 +35,7 @@ class JWTBaseMixin(serializers.Serializer):
def get_token(self): def get_token(self):
"""Create JWT token""" """Create JWT token"""
user = self.instance user = self.instance
token = RefreshToken.for_user(user) token = tokens.RefreshToken.for_user(user)
token['user'] = user.get_user_info() token['user'] = user.get_user_info()
return token return token
@ -178,6 +179,29 @@ class RefreshTokenSerializer(serializers.Serializer):
return data return data
class LogoutSerializer(serializers.ModelSerializer):
"""Serializer class for model Logout"""
class Meta:
model = BlacklistedAccessToken
fields = '__all__'
read_only_fields = [
'jti', 'token', 'user'
]
def create(self, validated_data, *args, **kwargs):
"""Override create method"""
request = self.context.get('request')
token = request._request.headers.get('Authorization')\
.split(' ')[::-1][0]
access_token = tokens.AccessToken(token)
# Prepare validated data
validated_data['user'] = request.user
validated_data['token'] = access_token.token
validated_data['jti'] = access_token.payload.get('jti')
return super().create(validated_data)
# OAuth # OAuth
class OAuth2Serialzier(BaseAuthSerializerMixin): class OAuth2Serialzier(BaseAuthSerializerMixin):
"""Serializer OAuth2 authorization""" """Serializer OAuth2 authorization"""

View File

@ -40,8 +40,8 @@ urlpatterns_jwt = [
path('refresh-token/', views.RefreshTokenView.as_view(), path('refresh-token/', views.RefreshTokenView.as_view(),
name="refresh-token"), name="refresh-token"),
# logout # logout
# path('logout/', views.LogoutView.as_view(), path('logout/', views.LogoutView.as_view(),
# name="logout"), name="logout"),
] ]

View File

@ -1,5 +1,6 @@
"""Common views for application Account""" """Common views for application Account"""
import json import json
from collections import namedtuple
from braces.views import CsrfExemptMixin from braces.views import CsrfExemptMixin
from django.conf import settings from django.conf import settings
@ -11,12 +12,9 @@ from rest_framework import generics
from rest_framework import permissions from rest_framework import permissions
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_simplejwt import tokens as jwt_tokens
from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore from rest_framework_social_oauth2.oauth2_backends import KeepRequestCore
from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer from rest_framework_social_oauth2.oauth2_endpoints import SocialTokenServer
from rest_framework_simplejwt import tokens as jwt_tokens
from rest_framework.settings import settings as rest_settings
from django.utils import timezone
from rest_framework_simplejwt.utils import datetime_to_epoch
from account.models import User from account.models import User
from authorization.models import Application from authorization.models import Application
@ -80,22 +78,50 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin):
# Login base view mixin # Login base view mixin
class JWTViewMixin(generics.GenericAPIView): class JWTViewMixin(generics.GenericAPIView):
"""JWT view mixin""" """JWT view mixin"""
def _handle_cookies(self, request, access_token, refresh_token):
"""
CHECK locale in cookies and PUT access and refresh tokens there.
_cookies it is list that contain tuples.
_cookies would contain key, value and secure parameters.
i.e.
[
(locale, 'ru-RU', True), # Key, Value, Secure flag
('access_token', 'token', True), # Key, Value, Secure flag
('refresh_token', 'token', True), # Key, Value, Secure flag
]
"""
cookies = list()
COOKIE = namedtuple('COOKIE', ['key', 'value', 'secure'])
if 'locale' in request.COOKIES:
# Write locale in cookie
_locale = COOKIE(key='locale', value=request.COOKIES.get('locale'), secure=False)
cookies.append(_locale)
# Write to cookie access and refresh token with secure flag
_access_token = COOKIE(key='access_token', value=access_token, secure=True)
_refresh_token = COOKIE(key='refresh_token', value=refresh_token, secure=True)
cookies.extend([_access_token, _refresh_token])
return cookies
def _put_cookies_in_response(self, cookies: list, response: Response):
"""Update COOKIES in response obj"""
for cookie in cookies:
response.set_cookie(key=cookie.key,
value=cookie.value,
secure=cookie.secure)
return response
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
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)
response = Response(serializer.data, status=status.HTTP_200_OK) response = Response(serializer.data, status=status.HTTP_200_OK)
if 'locale' in request.COOKIES: access_token = serializer.data.get('access_token')
# Write locale in cookie refresh_token = serializer.data.get('access_token')
key, value = 'locale', request.COOKIES.get('locale') return self._put_cookies_in_response(
response.set_cookie(key=key, value=value) cookies=self._handle_cookies(request, access_token, refresh_token),
# Write to cookie access and refresh token with secure flag response=response)
response.set_cookie(key='access_token',
value=serializer.data.get('access_token'),
secure=True)
response.set_cookie(key='refresh_token',
value=serializer.data.get('refresh_token'),
secure=True)
return response
# Serializers # Serializers
@ -176,22 +202,16 @@ class SignUpView(JWTViewMixin):
serializer_class = serializers.SignupSerializer serializer_class = serializers.SignupSerializer
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
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()
response = Response(serializer.data, status=status.HTTP_201_CREATED) response = Response(serializer.data, status=status.HTTP_201_CREATED)
if 'locale' in request.COOKIES: access_token = serializer.data.get('access_token')
# Write locale in cookie refresh_token = serializer.data.get('access_token')
key, value = 'locale', request.COOKIES.get('locale') return self._put_cookies_in_response(
response.set_cookie(key=key, value=value) cookies=self._handle_cookies(request, access_token, refresh_token),
# Write to cookie access and refresh token with secure flag response=response)
response.set_cookie(key='access_token',
value=serializer.data.get('access_token'),
secure=True)
response.set_cookie(key='refresh_token',
value=serializer.data.get('refresh_token'),
secure=True)
return response
# Login by username + password # Login by username + password
@ -209,38 +229,31 @@ class LoginByEmailView(JWTViewMixin):
# Refresh access_token # Refresh access_token
class RefreshTokenView(generics.GenericAPIView): class RefreshTokenView(JWTViewMixin):
"""Refresh access_token""" """Refresh access_token"""
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.RefreshTokenSerializer serializer_class = serializers.RefreshTokenSerializer
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""POST method"""
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)
response = Response(serializer.validated_data, status=status.HTTP_200_OK) response = Response(serializer.validated_data, status=status.HTTP_200_OK)
if 'locale' in request.COOKIES: access_token = serializer.data.get('access_token')
# Write locale in cookie refresh_token = serializer.data.get('access_token')
key, value = 'locale', request.COOKIES.get('locale') return self._put_cookies_in_response(
response.set_cookie(key=key, value=value) cookies=self._handle_cookies(request, access_token, refresh_token),
# Write to cookie access and refresh token with secure flag response=response)
response.set_cookie(key='access_token',
value=serializer.data.get('access_token'),
secure=True)
response.set_cookie(key='refresh_token',
value=serializer.data.get('refresh_token'),
secure=True)
return response
# Logout
# class LogoutView(generics.GenericAPIView):
# """Logout user"""
# permission_classes = (permissions.IsAuthenticated,)
#
# def post(self, request, *args, **kwargs):
# """POST method"""
# current_datetime = timezone.now()
# token = request.headers.get('Authorization').split(' ')[::-1][0]
# access_token = jwt_tokens.AccessToken(token)
# access_token.lifetime = timezone.timedelta(seconds=1)
# return Response(status=status.HTTP_200_OK)
# Logout
class LogoutView(generics.CreateAPIView):
"""Logout user"""
serializer_class = serializers.LogoutSerializer
def create(self, request, *args, **kwargs):
"""Override create method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_200_OK)

View File

@ -1,5 +1,7 @@
"""Utils app method.""" """Utils app method."""
import random import random
from django.http.request import HttpRequest
from rest_framework.request import Request
def generate_code(digits=6, string_output=True): def generate_code(digits=6, string_output=True):
@ -8,3 +10,12 @@ def generate_code(digits=6, string_output=True):
min_value = 10 ** (digits - 1) min_value = 10 ** (digits - 1)
value = random.randint(min_value, max_value) value = random.randint(min_value, max_value)
return str(value) if string_output else value return str(value) if string_output else value
def get_token_from_request(request):
"""Get access token from request"""
assert isinstance(request, (HttpRequest, Request))
if isinstance(request, HttpRequest):
return request.headers.get('Authorization').split(' ')[::-1][0]
elif isinstance(request, Request):
return request._request.headers.get('Authorization').split(' ')[::-1][0]

View File

@ -1,21 +1,21 @@
"""Project custom permissions""" """Project custom permissions"""
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from authorization.models import BlacklistedAccessToken
from utils.methods import get_token_from_request
class IsAuthenticatedAndHasRefreshToken(BasePermission): class IsAuthenticatedAndTokenIsValid(BasePermission):
""" """
Check if requested user is authenticated and has refresh token Check if user has a valid token and authenticated
""" """
def has_permission(self, request, view): def has_permission(self, request, view):
token = request.data.get('refresh_token') """Check permissions by access token and default rest permission IsAuthenticated"""
user = request.user user = request.user
if token and hasattr(user, 'oauth2_provider_refreshtoken'): token = get_token_from_request(request)
refresh_token_qs = user.oauth2_provider_refreshtoken blacklisted = BlacklistedAccessToken.objects.by_user(user)\
return ( .by_token(token)\
user.is_authenticated and .exists()
user.is_active and return bool(user and
refresh_token_qs.filter(token=token).exists() user.is_authenticated and
) not blacklisted)
else:
return False

View File

@ -208,9 +208,7 @@ REST_FRAMEWORK = {
'DEFAULT_VERSION': (AVAILABLE_VERSIONS['current'],), 'DEFAULT_VERSION': (AVAILABLE_VERSIONS['current'],),
'ALLOWED_VERSIONS': AVAILABLE_VERSIONS.values(), 'ALLOWED_VERSIONS': AVAILABLE_VERSIONS.values(),
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', 'utils.permissions.IsAuthenticatedAndTokenIsValid',
# todo: oauth2 scope + drf permission
# 'oauth2_provider.contrib.rest_framework.permissions.IsAuthenticatedOrTokenHasScope',
), ),
# 'DATETIME_FORMAT': '%m-%d-%Y %H:%M:%S', # experiment # 'DATETIME_FORMAT': '%m-%d-%Y %H:%M:%S', # experiment
# 'DATE_FORMAT': '%s.%f', # experiment # 'DATE_FORMAT': '%s.%f', # experiment
@ -231,12 +229,6 @@ AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
# todo: not used until outh2 scopes permission is enabled
# OAUTH2_PROVIDER = {
# # this is the list of available scopes
# 'SCOPES': {'read': 'Read scope', 'write': 'Write scope', 'groups': 'Access to your groups'}
# }
# Override default OAuth2 namespace # Override default OAuth2 namespace
DRFSO2_URL_NAMESPACE = 'auth' DRFSO2_URL_NAMESPACE = 'auth'
SOCIAL_AUTH_URL_NAMESPACE = 'auth' SOCIAL_AUTH_URL_NAMESPACE = 'auth'