version 0.0.7: new model, added endpoint ot logout, refactored auth views and serializers
This commit is contained in:
parent
68f6e0bc9c
commit
e830e30c90
29
apps/authorization/migrations/0002_blacklistedaccesstoken.py
Normal file
29
apps/authorization/migrations/0002_blacklistedaccesstoken.py
Normal 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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user