working on manual logout: block access token

This commit is contained in:
2025-06-02 12:18:13 +03:30
parent 8a2b2ce905
commit 0e4076e876
11 changed files with 152 additions and 42 deletions

View File

@@ -80,6 +80,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'apps.authentication.middlewares.BlockedTokenMiddleware',
'crum.CurrentRequestUserMiddleware',
'apps.log.middlewares.SaveLog'
]

View File

@@ -10,7 +10,7 @@ from apps.authentication.api.v1.serializers.serializer import (
OrganizationTypeSerializer,
OrganizationSerializer,
UserSerializer,
BankAccountSerializer
BankAccountSerializer,
)
from rest_framework_simplejwt.views import TokenObtainPairView
from apps.authorization.api.v1 import api as authorize_view
@@ -21,7 +21,8 @@ from apps.authentication.models import (
Province,
Organization,
OrganizationType,
BankAccountInformation
BankAccountInformation,
BlacklistedAccessToken
)
from django.db import transaction
from rest_framework.response import Response
@@ -30,6 +31,9 @@ from django.core.cache import cache
from rest_framework import status
from common.sms import send_sms
import random
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from apps.authentication.tools import get_token_jti
class CustomizedTokenObtainPairView(TokenObtainPairView):
@@ -37,6 +41,24 @@ class CustomizedTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomizedTokenObtainPairSerializer
class LogoutView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
token_str = request.auth # access token from header
jti, user_id = get_token_jti(str(token_str))
if not jti:
return Response({'detail': 'Invalid token'}, status=status.HTTP_400_BAD_REQUEST)
BlacklistedAccessToken.objects.get_or_create(jti=jti, defaults={
'token': token_str,
'user_id': user_id,
})
return Response({'detail': 'Access token blacklisted.'}, status=status.HTTP_200_OK)
class UserViewSet(ModelViewSet):
""" Crud operations for user model """
queryset = User.objects.all()

View File

@@ -13,7 +13,8 @@ from .api import (
ProvinceViewSet,
OrganizationViewSet,
OrganizationTypeViewSet,
GeneralOTPViewSet
GeneralOTPViewSet,
LogoutView
)
router = DefaultRouter()
@@ -26,6 +27,7 @@ router.register(r'otp', GeneralOTPViewSet, basename='otp')
urlpatterns = [
path('login/', CustomizedTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('logout/', LogoutView.as_view(), name='logut'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
path('token/revoke/', TokenBlacklistView.as_view(), name='revoke_token'),

View File

@@ -0,0 +1,9 @@
from rest_framework.exceptions import APIException
from django.utils.translation import gettext_lazy as _
from rest_framework import status
class TokenBlackListedException(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = _('unauthorized')
default_code = 'unauthorized'

View File

@@ -0,0 +1,25 @@
from django.utils.deprecation import MiddlewareMixin
from .models import BlacklistedAccessToken
from apps.authentication.tools import get_token_jti
from rest_framework.exceptions import AuthenticationFailed
from apps.authentication.exceptions import TokenBlackListedException
from rest_framework.response import Response
from django.http import JsonResponse
from rest_framework import status
class BlockedTokenMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token_str = auth_header[7:]
jti, _ = get_token_jti(token_str)
if jti and BlacklistedAccessToken.objects.filter(jti=jti).exists():
return JsonResponse({
'detail': 'Access token has been blacklisted'
}, status=401)
return self.get_response(request)

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0 on 2025-06-02 08:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0019_organizationtype_code'),
]
operations = [
migrations.CreateModel(
name='BlacklistedAccessToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('jti', models.CharField(max_length=255, unique=True)),
('token', models.TextField()),
('user_id', models.IntegerField()),
('blacklisted_at', models.DateTimeField(auto_now_add=True)),
],
),
]

View File

@@ -144,3 +144,13 @@ class BankAccountInformation(BaseModel):
def save(self, *args, **kwargs):
super(BankAccountInformation, self).save(*args, **kwargs)
class BlacklistedAccessToken(models.Model):
jti = models.CharField(max_length=255, unique=True)
token = models.TextField()
user_id = models.IntegerField()
blacklisted_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Blacklisted JTI: {self.jti}"

View File

@@ -0,0 +1,9 @@
from rest_framework_simplejwt.tokens import AccessToken
def get_token_jti(token_str):
try:
token = AccessToken(token_str)
return token['jti'], token['user_id']
except Exception as e:
return None, None

View File

@@ -3,14 +3,16 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from apps.authorization.api.v1.serializers import (
RoleSerializer,
PermissionSerializer,
UserRelationSerializer
UserRelationSerializer,
PageSerializer
)
from rest_framework.decorators import action
from rest_framework.response import Response
from apps.authorization.models import (
Role,
Permissions,
UserRelations
UserRelations,
Page
)
from rest_framework import viewsets
from django.db import transaction
@@ -23,27 +25,22 @@ class RoleViewSet(viewsets.ModelViewSet):
serializer_class = RoleSerializer
class PageViewSet(viewsets.ModelViewSet):
""" add website pages to system to set permission on it """
queryset = Page.objects.all()
serializer_class = PageSerializer
class PermissionViewSet(viewsets.ModelViewSet):
""" Crud Operations for Permissions """
queryset = Permissions.objects.all()
serializer_class = PermissionSerializer
@action(
methods=['get'],
detail=False,
url_path='get_user_permissions',
url_name='get_user_permissions',
name='get_user_permissions'
)
@transaction.atomic
def get_user_permissions(self, request):
pass
class UserRelationViewSet(viewsets.ModelViewSet):
""" Crud Operations for User Relations """
queryset = UserRelations.objects.all()
serializer_class = UserRelationSerializer

View File

@@ -1,14 +1,26 @@
import typing
from rest_framework import serializers
from apps.authorization.models import (
Role,
Permissions,
UserRelations
UserRelations,
Page
)
from apps.authentication.api.v1.serializers import serializer as auth_serializer
from apps.authentication.models import Organization
import itertools
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = [
'name',
'code'
]
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permissions
@@ -26,6 +38,18 @@ class PermissionSerializer(serializers.ModelSerializer):
return representation
@classmethod
def permissions_structure_output(cls, permissions: list) -> typing.Any:
""" set a structure for permissions """
structure = {}
for permission in permissions:
if permission.page.name not in structure.keys():
structure.update(
{f'{permission.page.name}': itertools.chain(*list(
permission.page.permission_page.all().values_list('name')))
})
return structure
class RoleSerializer(serializers.ModelSerializer):
class Meta:
@@ -49,15 +73,7 @@ class RoleSerializer(serializers.ModelSerializer):
representation['type'] = auth_serializer.OrganizationTypeSerializer(instance.type).data
if instance.permissions: # noqa
permissions = instance.permissions.all()
pages = {}
for permission in permissions:
if permission.page.name not in pages.keys():
pages.update({
f'{permission.page.name}': itertools.chain(*list(
(permission.page.permission_page.all().values_list('name'))
))
})
representation['permissions'] = pages
representation['permissions'] = PermissionSerializer().permissions_structure_output(permissions)
return representation
@@ -73,6 +89,7 @@ class UserRelationSerializer(serializers.ModelSerializer):
]
def to_representation(self, instance):
""" custom output for serializer """
representation = super().to_representation(instance)
if isinstance(instance, UserRelations):
if instance.user:
@@ -81,18 +98,11 @@ class UserRelationSerializer(serializers.ModelSerializer):
representation['organization'] = auth_serializer.OrganizationSerializer(instance.organization).data
if instance.role:
representation['role'] = RoleSerializer(instance.role).data
if instance.permissions:
if instance.permissions: # noqa
# set permissions by a default structure like:
# 'page permission':[element permissions]
permissions = instance.permissions.all()
pages = {}
for permission in permissions:
if permission.page.name not in pages.keys():
pages.update({
f'{permission.page.name}': itertools.chain(*list(
(permission.page.permission_page.all().values_list('name'))
))
})
representation['permissions'] = pages
representation['permissions'] = PermissionSerializer().permissions_structure_output(permissions)
return representation
def update(self, instance, validated_data):
@@ -100,9 +110,9 @@ class UserRelationSerializer(serializers.ModelSerializer):
if validated_data.get('role'):
instance.role = Role.objects.get(id=validated_data.get('role', instance.role))
if validated_data.get('organization'):
instance.organization = Organization.objects.get(id=validated_data.get(
'organization', instance.organization
))
instance.organization = Organization.objects.get(
id=validated_data.get('organization', instance.organization)
)
instance.save()
instance.permissions.clear()
instance.permissions.add(*(validated_data.get('permissions', instance.permissions)))

View File

@@ -3,7 +3,8 @@ from django.urls import path, include
from .api import (
RoleViewSet,
PermissionViewSet,
UserRelationViewSet
UserRelationViewSet,
PageViewSet
)
router = DefaultRouter() # set router
@@ -12,6 +13,7 @@ router = DefaultRouter() # set router
router.register(r'role', RoleViewSet, basename='role')
router.register(r'permission', PermissionViewSet, basename='permission')
router.register(r'user-relations', UserRelationViewSet, basename='organization-role')
router.register(r'page', PageViewSet, basename='page')
urlpatterns = [
path('', include(router.urls))