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 oauth2_provider import models as oauth2_models
from oauth2_provider.models import AbstractApplication
from django.utils.translation import gettext_lazy as _
from utils.models import PlatformMixin
@ -30,3 +31,42 @@ class Application(PlatformMixin, AbstractApplication):
def natural_key(self):
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 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 rest_framework_simplejwt.tokens import RefreshToken
# JWT
from rest_framework_simplejwt.tokens import RefreshToken, SlidingToken, UntypedToken
from rest_framework_simplejwt import tokens
JWT_SETTINGS = settings.SIMPLE_JWT
@ -34,7 +35,7 @@ class JWTBaseMixin(serializers.Serializer):
def get_token(self):
"""Create JWT token"""
user = self.instance
token = RefreshToken.for_user(user)
token = tokens.RefreshToken.for_user(user)
token['user'] = user.get_user_info()
return token
@ -178,6 +179,29 @@ class RefreshTokenSerializer(serializers.Serializer):
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
class OAuth2Serialzier(BaseAuthSerializerMixin):
"""Serializer OAuth2 authorization"""

View File

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

View File

@ -1,5 +1,6 @@
"""Common views for application Account"""
import json
from collections import namedtuple
from braces.views import CsrfExemptMixin
from django.conf import settings
@ -11,12 +12,9 @@ from rest_framework import generics
from rest_framework import permissions
from rest_framework import status
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_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 authorization.models import Application
@ -80,22 +78,50 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin):
# Login base view mixin
class JWTViewMixin(generics.GenericAPIView):
"""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):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_200_OK)
if 'locale' in request.COOKIES:
# Write locale in cookie
key, value = 'locale', request.COOKIES.get('locale')
response.set_cookie(key=key, value=value)
# Write to cookie access and refresh token with secure flag
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
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('access_token')
return self._put_cookies_in_response(
cookies=self._handle_cookies(request, access_token, refresh_token),
response=response)
# Serializers
@ -176,22 +202,16 @@ class SignUpView(JWTViewMixin):
serializer_class = serializers.SignupSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
response = Response(serializer.data, status=status.HTTP_201_CREATED)
if 'locale' in request.COOKIES:
# Write locale in cookie
key, value = 'locale', request.COOKIES.get('locale')
response.set_cookie(key=key, value=value)
# Write to cookie access and refresh token with secure flag
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
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('access_token')
return self._put_cookies_in_response(
cookies=self._handle_cookies(request, access_token, refresh_token),
response=response)
# Login by username + password
@ -209,38 +229,31 @@ class LoginByEmailView(JWTViewMixin):
# Refresh access_token
class RefreshTokenView(generics.GenericAPIView):
class RefreshTokenView(JWTViewMixin):
"""Refresh access_token"""
permission_classes = (permissions.IsAuthenticated,)
serializer_class = serializers.RefreshTokenSerializer
def post(self, request, *args, **kwargs):
"""POST method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.validated_data, status=status.HTTP_200_OK)
if 'locale' in request.COOKIES:
# Write locale in cookie
key, value = 'locale', request.COOKIES.get('locale')
response.set_cookie(key=key, value=value)
# Write to cookie access and refresh token with secure flag
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)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('access_token')
return self._put_cookies_in_response(
cookies=self._handle_cookies(request, access_token, refresh_token),
response=response)
# 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."""
import random
from django.http.request import HttpRequest
from rest_framework.request import Request
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)
value = random.randint(min_value, max_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"""
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):
token = request.data.get('refresh_token')
"""Check permissions by access token and default rest permission IsAuthenticated"""
user = request.user
if token and hasattr(user, 'oauth2_provider_refreshtoken'):
refresh_token_qs = user.oauth2_provider_refreshtoken
return (
user.is_authenticated and
user.is_active and
refresh_token_qs.filter(token=token).exists()
)
else:
return False
token = get_token_from_request(request)
blacklisted = BlacklistedAccessToken.objects.by_user(user)\
.by_token(token)\
.exists()
return bool(user and
user.is_authenticated and
not blacklisted)

View File

@ -208,9 +208,7 @@ REST_FRAMEWORK = {
'DEFAULT_VERSION': (AVAILABLE_VERSIONS['current'],),
'ALLOWED_VERSIONS': AVAILABLE_VERSIONS.values(),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
# todo: oauth2 scope + drf permission
# 'oauth2_provider.contrib.rest_framework.permissions.IsAuthenticatedOrTokenHasScope',
'utils.permissions.IsAuthenticatedAndTokenIsValid',
),
# 'DATETIME_FORMAT': '%m-%d-%Y %H:%M:%S', # experiment
# 'DATE_FORMAT': '%s.%f', # experiment
@ -231,12 +229,6 @@ AUTHENTICATION_BACKENDS = (
'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
DRFSO2_URL_NAMESPACE = 'auth'
SOCIAL_AUTH_URL_NAMESPACE = 'auth'