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 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)
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user