Рубрики
Без рубрики

Аутентификация JWT с Django Rest Framework – Что? Почему? Как?

Привет сообщество, у меня было много проблем с пониманием использования механизмов аутентификации и … Tagged с Django, DRF, JWT, Python.

HI Community, у меня было много проблем с пониманием использования механизмов аутентификации и обеспечения конечных точек API до недавнего времени, когда я должен был внедрить аутентификацию JWT для обеспечения защиты API, используемых в моем проекте. Мне пришлось провести достаточное количество чтения и исследований, которые заставили меня хотеть нанести удар, чтобы помочь другим понять, что такое аутентификация JWT, почему она необходима и как вы можете реализовать его в своем проекте, чтобы обеспечить аутентификацию пользователя.

Мой подход к выполнению аутентификации был на следующих строках –

Мой API REST должен попросить имя пользователя и пароль. После предоставления он будет использоваться для фильтрации базы данных, чтобы проверить, существует ли пользователь с этими учетными данными.

Звучал для меня логично.

Но настоящая проблема – это без гражданства природы протокола HTTP Анкет Это означало, что в любое время был выдан новый запрос, пользователь, выдающий запрос, должен снова быть аутентифицирован. К счастью, у Джанго есть своя система аутентификации на основе сеансов. В Джанго «Сессии» хранятся в виде печенья. Эта аутентификация на основе сессии является государственной. Каждый раз, когда клиент запрашивает сервер, сервер ловит сеанс в памяти, чтобы отобразить идентификатор сеанса обратно обратному запрошему пользователю. Эти сеансы гарантируют, что пользователь возвращается каждый раз, когда выполняется запрос. Пользователь можно получить как request.user Анкет

Итак, если у Джанго есть система аутентификации сеанса по умолчанию, то в чем же проблема?

Аутентификация Джанго работает только с традиционным циклом запроса-ответа HTML

Это означает, что в любое время, когда запрос вносится на сервер, сервер будет иметь элемент управления для обработки этого запроса и ответит HTML. Проблема заключается в том, что приложение на стороне клиента не следует традиционному циклу ответов на запрос HTML. Вместо этого клиент ожидает, что сервер вернет JSON вместо HTML. Возвращаясь JSON, мы можем позволить клиенту решить, что он должен делать дальше, вместо того, чтобы позволить серверу решать. Таким образом, ответ JSON не контролирует поведение браузера, он просто возвращает результат сделанного запроса.

Еще одна проблема с аутентификацией на основе сеансов Django – это обрабатывать различные домены для клиента и сервера, что затрудняет использование сеансов. Это означает, что, поскольку сеансы Django хранятся в файлах cookie, позволяя внешним доменам доступ к файлам cookie, сделают его уязвимым для атак CSRF. Атаки CSRF – это события, в которых аутентифицированные пользователи обманывают в совершении вредоносных действий, когда злоумышленник получает доступ к этим хранимым файлам cookie Анкет Более того, DRF отключает обработку CSRF для Apiviews Затруднить полагаться на аутентификацию на основе сеансов (особенно при использовании таможенных моделей, шаблонов и представлений)

Наиболее распространенной альтернативой аутентификации на основе сеансов является система аутентификации на основе токков. Аутентификация на основе токков, как следует из названия, генерирует токен (сервером) каждый раз, когда пользователь успешно входит в систему, отображая токен, сгенерированный с пользователем и хранит его в LocalStorage на стороне клиента. Теперь ожидается, что клиент отправит этот сгенерированный токен в качестве заголовка для каждого последующего запроса для аутентификации пользователя. Если токен для указанного пользователя существует в LocalStorage, то пользователь аутентифицируется. Поскольку этот токен является единственным требованием для сервера проверить личность пользователя, он без сохранения состояния. Самая популярная аутентификация на основе токенов с API REST – это аутентификация JWT (JSON Web Token). Это закодированное (безопасное) представление претензий между двумя сторонами.

Звучит довольно просто, но-

Хранение сгенерированного токена в LocalStorage делает систему уязвимой для атак XSS. Атаки XSS – это тип вредоносных инъекций через сторону клиента, которые могут легко манипулировать LocalStorage

И ЧТО ТЕПЕРЬ?

  1. Нужно найти способ хранить токены на стороне клиента (не может использовать LocalStorage, поскольку он подвергается атакам XSS)
  2. Не могу использовать сеанс, так как файлы cookie склонны к атакам CSRF.

Давайте поймем поток процесса наряду с кодом.

Шаг 1 : Установите свой проект (я позвоню в мой jwtauth) и установите следующие зависимости:

pip install django django-cors-headers djangorestframework PyJWT

django-admin startproject jwtauth
cd jwtauth
django-admin startapp demo

Шаг 1 : Измените свой файл jwtauth/stens.py

INSTALLED_APPS = [
    ...
    'corsheaders',
    'rest_framework',
    ...
    'app_name' #demo in my example

]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

CORS_ALLOW_CREDENTIALS = True 
CORS_ORIGIN_WHITELIST = ['http://localhost:3000']
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
     'demo.verify.JWTAuthentication',
    ),    
    'DEFAULT_PERMISSION_CLASSES': (
    'rest_framework.permissions.IsAuthenticated',
    )
}

CORS_ALLY_CREDENTIALS позволяет отправлять файлы cookie в междоменных ответах Cors_origin_whitelist = [‘ http://localhost: 3000 ‘ ] домен для вашего фронтального приложения или где ваш клиент.

Шаг 2 : Создать суперпользователь

python manage.py createsuper

Шаг 3 : Создать пользовательский сериализатор, чтобы пользовательский API мог вернуть детали как объект JSON

from rest_framework import serializers
from django.contrib.auth import get_user_model

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = get_user_model()
        fields = ['id', 'username', 'email','first_name','is_active']

Шаг 4 : Создать новый файл Python в Demo As Auth.py (demo/auth.py), чтобы генерировать и обновить токены доступа

import datetime
import jwt
from django.conf import settings

def generate_access_token(user):

    access_token_payload = {
        'user_id': user.id,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(days=0, minutes=5),
        'iat': datetime.datetime.utcnow(),
    }
    access_token = jwt.encode(access_token_payload,
                              settings.SECRET_KEY, algorithm='HS256')
    return access_token


def generate_refresh_token(user):
    refresh_token_payload = {
        'user_id': user.id,
        'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7),
        'iat': datetime.datetime.utcnow()
    }
    refresh_token = jwt.encode(
        refresh_token_payload, settings.SECRET_KEY, algorithm='HS256')

    return refresh_token

Шаг 5 : Создать представление пользователя и URL в Demo/views.py и demo/urls.py соответственно

from django.shortcuts import render
from django.contrib.auth.models import User
from .serializers import UserSerializer
from rest_framework import viewsets
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.contrib.auth import get_user_model
from rest_framework import exceptions
from rest_framework.permissions import AllowAny
from rest_framework.decorators import api_view, permission_classes
from django.views.decorators.csrf import ensure_csrf_cookie
from .auth import generate_access_token, generate_refresh_token
from django.views.decorators.csrf import csrf_protect
import jwt
from django.conf import settings

@api_view(['GET'])
def user(request):
    user = request.user
    serialized_user = UserSerializer(user).data
    return Response({'user': serialized_user })
from django.urls import include, path
from .views import user,login_view
from rest_framework.routers import DefaultRouter


urlpatterns = [
    path('user', user, name='user'),

Шаг 6 : Создать представление и URL в демо/views.py и demo/urls.py соответственно

@api_view(['POST'])
@permission_classes([AllowAny])
@ensure_csrf_cookie
def login_view(request):
    User = get_user_model()
    username = request.data.get('username')
    password = request.data.get('password')
    response = Response()
    if (username is None) or (password is None):
        raise exceptions.AuthenticationFailed(
            'username and password required')

    user = User.objects.filter(username=username).first()
    if(user is None):
        raise exceptions.AuthenticationFailed('user not found')
    if (not user.check_password(password)):
        raise exceptions.AuthenticationFailed('wrong password')

    serialized_user = UserSerializer(user).data

    access_token = generate_access_token(user)
    refresh_token = generate_refresh_token(user)

    response.set_cookie(key='refreshtoken', value=refresh_token, httponly=True)
    response.data = {
        'access_token': access_token,
        'user': serialized_user,
    }

    return response
from django.urls import include, path
from .views import user,login_view
from rest_framework.routers import DefaultRouter


urlpatterns = [
    path('user', user, name='user'),
    path('login',login_view,name='login'),
]

Чтобы понять этот шаг, имеет решающее значение.

Конечная точка API login_view имеет три декоратора:

@api_view ([‘post’]) – Позволяет запрос сообщения с именем пользователя и паролем в теле запроса @permission_classes ([allonsany]) – делает просмотр входа в систему public @ensure_csrf_cookie – обеспечивает соблюдение DRF для отправки CSRF Cookie в качестве ответа в случае успешного входа в систему

В настоящее время,

  1. У нас есть Access_token в теле ответа
  2. обновления как httponly cookie
  3. токен CSRF в качестве обычного печенья, который можно легко поглотить

Теперь давайте попробуем проверить это –

Перейти к http://127.0.0.1:8000/users/login и введите имя пользователя и пароль в тело запроса. Убедитесь, что пользователь с этими учетными данными зарегистрирован в вашей базе данных, если не перейти к http://127.0.0.1:8000/admin/ и создайте набор тестовых пользователей, входя в систему с учетными данными SuperUser.

Успешная запись генерирует следующий ответ –

Если вы ударили http://127.0.0.1:8000/users/login На почтальце вы сможете увидеть следующее-

что мы успешно сгенерировали два файла cookie

  1. обновления как httponly cookie
  2. токен CSRF как обычное печенье

Шаг 7 : Создать новый файл Python как veserify.py в разделе Demo (demo/verify.py)

Поскольку DRF обеспечивает соблюдение CSRF только в аутентификации сеанса, нам необходимо обеспечить обеспечение соблюдения CSRF для просмотров API, вот где декоратор @ensure_csrf_cookie входит в картинку.

Давайте определим, что в нашем пользовательском файле аутентификации verify.py

import jwt
from rest_framework.authentication import BaseAuthentication
from django.middleware.csrf import CsrfViewMiddleware
from rest_framework import exceptions
from django.conf import settings
from django.contrib.auth import get_user_model


class CSRFCheck(CsrfViewMiddleware):
    def _reject(self, request, reason):
        return reason


class JWTAuthentication(BaseAuthentication):

    def authenticate(self, request):

        User = get_user_model()
        authorization_heaader = request.headers.get('Authorization')

        if not authorization_heaader:
            return None
        try:
            access_token = authorization_heaader.split(' ')[1]
            payload = jwt.decode(
                access_token, settings.SECRET_KEY, algorithms=['HS256'])

        except jwt.ExpiredSignatureError:
            raise exceptions.AuthenticationFailed('access_token expired')
        except IndexError:
            raise exceptions.AuthenticationFailed('Token prefix missing')

        user = User.objects.filter(id=payload['user_id']).first()
        if user is None:
            raise exceptions.AuthenticationFailed('User not found')

        if not user.is_active:
            raise exceptions.AuthenticationFailed('user is inactive')

        self.enforce_csrf(request)
        return (user, None)

    def enforce_csrf(self, request):
        check = CSRFCheck()
        check.process_request(request)
        reason = check.process_view(request, None, (), {})
        print(reason)
        if reason:
            raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)

Еще раз откройте почтальон, и под вкладкой «Заголовки» добавьте следующее в качестве пары значений ключей, предоставьте значение в следующем формате токен access_token сгенерирован

Затем вы можете столкнуться с следующей ошибкой –

Это связано с тем, что Frontend должен передавать токен CSRF, сгенерированный обратно на сервер в заголовке, но для целей тестирования вы можете выполнить вручную, добавляя пару значений x -csrftoken –

Шаг 8 : Создайте представление токена обновления в Demo/views.py и добавьте его URL в Demo/urls.py

@api_view(['POST'])
@permission_classes([AllowAny])
@csrf_protect
def refresh_token_view(request):
    User = get_user_model()
    refresh_token = request.COOKIES.get('refreshtoken')
    if refresh_token is None:
        raise exceptions.AuthenticationFailed(
            'Authentication credentials were not provided.')
    try:
        payload = jwt.decode(
            refresh_token, settings.REFRESH_TOKEN_SECRET, algorithms=['HS256'])
    except jwt.ExpiredSignatureError:
        raise exceptions.AuthenticationFailed(
            'expired refresh token, please login again.')

    user = User.objects.filter(id=payload.get('user_id')).first()
    if user is None:
        raise exceptions.AuthenticationFailed('User not found')

    if not user.is_active:
        raise exceptions.AuthenticationFailed('user is inactive')


    access_token = generate_access_token(user)
    return Response({'access_token': access_token})

Важными моментами, которые следует отметить, являются создание нового токена доступа, который ожидает запрос –

  1. Cookie, который содержит действительный rupresh_token
  2. Заголовок ‘x-csrftoken’ с действительным токеном CSRF
  3. Только если represh_token недействителен или истек, пользователю необходимо будет повторно логин.

Таким образом, если все работало нормально, вы должны увидеть что-то на одних и тех же линиях-

Мне было очень трудно понять весь механизм обеспечения конечных точек и почему это действительно необходимо. Через этот пост я надеюсь, что смогу помочь таким людям, как я, изо всех сил пытаясь понять и реализовать аутентификацию JWT самым оптимальным способом. Как всегда, я открыт для мыслей, запросов и дискуссий. Я хотел бы понять, есть ли более оптимальный подход к проблеме.

  1. Исходный код – https://dev.to/a_atalla/django-rest-framework-custom-jwt-authentication-5n5 (немного изменили это в этом посте)

  2. Разница между механизмами на основе сеансов и токенов – https://dev.to/thecodearcher/what-really-is-the-he-between-session-and-token на основе Authentication-2o39

  3. Цикл запроса -ответа в Django – https://medium.com/@ksarthak4ever/django-request-response-cycle-2626e9e8606e

  4. Атаки CSRF – https://dev.to/_smellycode/csrf-in-action-21n3

  5. XSS атаки – https://dev.to/kmistele/xss-what-it-is-how-it-works-and-how-to-prevent-it-589o

  6. Почему в Джанго? https://stackoverflow.com/questions/31600497/django-drf-token-based-authentication-vs-json-web-token

Оригинал: “https://dev.to/bhavanaeh/jwt-authentication-with-django-rest-framework-what-why-how-50kj”