Django OAuth2 Authentication Failing in Unit Tests - 401 'invalid_client' Error

Я застрял на этой проблеме уже 3 дня, и я надеюсь, что кто-нибудь сможет мне помочь. Я пытаюсь реализовать OAuth2 аутентификацию для моего Django бэкенда с REST интерфейсом. Однако мои модульные тесты для аутентификации не работают, и я получаю ошибку 401 с сообщением 'invalid_client' вместо ожидаемого кода состояния 200.

Моя модель пользователя настроена на использование электронной почты в качестве идентификатора пользователя. Вот соответствующая часть моей модели пользователя:

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(auto_now_add=True)
    roles = models.ManyToManyField(UserRole, related_name="roles")

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('The Email field must be set')
        email = self.normalize_email(email)
        user = self.model( email=email, **extra_fields)
        if password:
            user.set_password(password)
        else:
            user.set_unusable_password()
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self.create_user(email, password, **extra_fields)`

Мои настройки Django

from .settings import *

# Use an in-memory SQLite database for testing
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}

# Other testing-specific settings
DEBUG = True

TIME_ZONE = 'UTC'
USE_TZ = True

# Middleware to run migrations automatically
class RunMigrationsMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.run_migrations()

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def run_migrations(self):
        from django.core.management import call_command
        call_command('migrate')

MIDDLEWARE = [
    'matchplan.test_settings.RunMigrationsMiddleware', 
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'oauth2_provider.middleware.OAuth2TokenMiddleware',  
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# Django REST Framework settings
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

# Custom test runner
TEST_RUNNER = 'matchplan.test_runner.CustomTestRunner'

И, наконец, модульный тест

from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
from oauth2_provider.models import Application
from django.urls import reverse

class OAuth2Test(APITestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            email='testuser@example.com',  # Provide the email argument
            password='testpass123'
        )
        self.application = Application.objects.create(
            name='Test Application',
            client_type=Application.CLIENT_CONFIDENTIAL,
            authorization_grant_type=Application.GRANT_PASSWORD,
            user=self.user
        )

    def test_password_grant(self):
        token_url = reverse('oauth2_provider:token')
        data = {
            'grant_type': 'password',
            'username': 'testuser@example.com',  # Use the email as the username
            'password': 'testpass123',
            'client_id': self.application.client_id,
            'client_secret': self.application.client_secret,
        }
        # Debug prints
        print(f"Client Type: {self.application.client_type}")
        print(f"Client ID: {self.application.client_id}")
        print(f"Client Secret: {self.application.client_secret}")

        response = self.client.post(token_url, data, format='json')
        print(response.json())  # Use response.json() to get the JSON data
        self.assertEqual(response.status_code, 200) # this assertion fails with 401

Я перепроверил конфигурацию OAuth2 и учетные данные клиента, но не могу понять, почему аутентификация не проходит в модульных тестах. Любая помощь или предложения будут высоко оценены!

Дополнительная информация

  • Django версия: 5.1
  • Django REST framework version: 3.15.2
  • Используемая библиотека OAuth2: django-oauth-toolkit

Тест проваливается, возвращая код состояния 401 с сообщением об ошибке 'invalid_client'.

Проблема возникает на этапе проверки хэша с помощью django auth hasher. При создании oauth2 приложения, oauth2 автоматически создает хэш для секрета клиента, затем этот секрет (тот, что до хэша) должен быть предоставлен в полезной нагрузке. Т.е. если секрет youllneverguess, а хэш pbkdf2_sha256$7200...., то он ожидает, что секрет youllneverguess (не хэшированный секрет) будет предоставлен в полезной нагрузке с идентификатором клиента, который используется при получении oauth2 приложения. Затем он кодирует предоставленный секрет (youllneverguess) и сравнивает хэш с тем, который хранится в найденном приложении.

поэтому ваше решение будет таким:

class OAuth2Test(APITestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            email='testuser@example.com',  # Provide the email argument
            password='testpass123'
        ) # type: ignore
        self.application = Application.objects.create(
            name='Test Application',
            client_type=Application.CLIENT_CONFIDENTIAL,
            authorization_grant_type=Application.GRANT_PASSWORD,
            user=self.user,
            client_secret="youcanneverguess" # this can be an environment variable
        )

    def test_password_grant(self):
        token_url = reverse('oauth2_provider:token')
        data = {
            'grant_type': "password",
            'username': 'testuser@example.com',
            'password': 'testpass123',
            'client_id': self.application.client_id,
            # don't use self.application.client_secret since the field will return the hashed value 
            'client_secret': "youcanneverguess", # this can be an environment variable
        }

        response = self.client.post(token_url, data=data, format='json')
        print(response.json())
        self.assertEqual(response.status_code, 200)
Вернуться на верх