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)