This commit is contained in:
Nikita 2025-01-06 14:35:48 -08:00
parent 52b1b7150d
commit f37470b648
4 changed files with 94 additions and 2 deletions

View File

@ -34,6 +34,17 @@ The authentication schemes are always defined as a list of classes. REST framew
If no class authenticates, `request.user` will be set to an instance of `django.contrib.auth.models.AnonymousUser`, and `request.auth` will be set to `None`. If no class authenticates, `request.user` will be set to an instance of `django.contrib.auth.models.AnonymousUser`, and `request.auth` will be set to `None`.
The value of `request.user` and `request.auth` for unauthenticated requests can be modified using the `UNAUTHENTICATED_USER` and `UNAUTHENTICATED_TOKEN` settings. The value of `request.user` and `request.auth` for unauthenticated requests can be modified using the `UNAUTHENTICATED_USER` and `UNAUTHENTICATED_TOKEN` settings.
### MultiUserModelAuthentication
The `MultiUserModelAuthentication` class supports authentication for multiple user models.
To use this authentication mechanism, add it to your `DEFAULT_AUTHENTICATION_CLASSES` in `settings.py`:
```python
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.MultiUserModelAuthentication',
],
}
## Setting the authentication scheme ## Setting the authentication scheme

View File

@ -230,3 +230,63 @@ class RemoteUserAuthentication(BaseAuthentication):
user = authenticate(request=request, remote_user=request.META.get(self.header)) user = authenticate(request=request, remote_user=request.META.get(self.header))
if user and user.is_active: if user and user.is_active:
return (user, None) return (user, None)
class MultiUserModelAuthentication(BaseAuthentication):
"""
Custom authentication to support multiple user models.
"""
def authenticate(self, request):
"""
Authenticate the request for multiple user models.
Returns a tuple of (user, None) or raises an exception if authentication fails.
"""
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'basic':
return None
if len(auth) == 1:
msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
try:
auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
except UnicodeDecodeError:
auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
userid, password = auth_decoded.split(':', 1)
except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(userid, password, request)
def authenticate_credentials(self, userid, password, request=None):
"""
Authenticate credentials for multiple user models.
"""
# List of user models to authenticate against
user_models = ['users.User', 'admins.AdminUser']
for model_name in user_models:
try:
UserModel = get_user_model() # Replace with custom logic for each model
credentials = {UserModel.USERNAME_FIELD: userid, 'password': password}
user = authenticate(request=request, **credentials)
if user:
if not user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (user, None)
except Exception as e:
# Continue to next user model if the current one fails
continue
raise exceptions.AuthenticationFailed(_('Invalid username/password for all user models.'))
def authenticate_header(self, request):
return 'Basic realm="api"'

View File

@ -38,6 +38,7 @@ DEFAULTS = {
'rest_framework.parsers.MultiPartParser' 'rest_framework.parsers.MultiPartParser'
], ],
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.MultiUserModelAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication' 'rest_framework.authentication.BasicAuthentication'
], ],

View File

@ -6,7 +6,10 @@ from django.contrib.auth.models import User
from django.http import HttpResponse from django.http import HttpResponse
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.urls import include, path from django.urls import include, path
from rest_framework.test import APIClient
from django.test import TestCase
from users.models import User
from admins.models import AdminUser
from rest_framework import ( from rest_framework import (
HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status
) )
@ -597,3 +600,20 @@ class RemoteUserAuthenticationUnitTests(TestCase):
response = self.client.post('/remote-user/', response = self.client.post('/remote-user/',
REMOTE_USER=self.username) REMOTE_USER=self.username)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
class MultiUserModelAuthenticationTest(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(username='user', password='userpass')
self.admin = AdminUser.objects.create_user(username='admin', password='adminpass')
def test_user_authentication(self):
response = self.client.post('/api/token/', {'username': 'user', 'password': 'userpass'})
self.assertEqual(response.status_code, 200)
def test_admin_authentication(self):
response = self.client.post('/api/token/', {'username': 'admin', 'password': 'adminpass'})
self.assertEqual(response.status_code, 200)
def test_invalid_authentication(self):
response = self.client.post('/api/token/', {'username': 'invalid', 'password': 'invalid'})
self.assertEqual(response.status_code, 401)