Skip to content

Commit f37470b

Browse files
solve #9598
1 parent 52b1b71 commit f37470b

File tree

4 files changed

+94
-2
lines changed

4 files changed

+94
-2
lines changed

docs/api-guide/authentication.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ The authentication schemes are always defined as a list of classes. REST framew
3434
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`.
3535

3636
The value of `request.user` and `request.auth` for unauthenticated requests can be modified using the `UNAUTHENTICATED_USER` and `UNAUTHENTICATED_TOKEN` settings.
37+
### MultiUserModelAuthentication
38+
The `MultiUserModelAuthentication` class supports authentication for multiple user models.
39+
40+
To use this authentication mechanism, add it to your `DEFAULT_AUTHENTICATION_CLASSES` in `settings.py`:
41+
42+
```python
43+
REST_FRAMEWORK = {
44+
'DEFAULT_AUTHENTICATION_CLASSES': [
45+
'rest_framework.authentication.MultiUserModelAuthentication',
46+
],
47+
}
3748

3849
## Setting the authentication scheme
3950

rest_framework/authentication.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,63 @@ def authenticate(self, request):
230230
user = authenticate(request=request, remote_user=request.META.get(self.header))
231231
if user and user.is_active:
232232
return (user, None)
233+
class MultiUserModelAuthentication(BaseAuthentication):
234+
"""
235+
Custom authentication to support multiple user models.
236+
"""
237+
238+
def authenticate(self, request):
239+
"""
240+
Authenticate the request for multiple user models.
241+
Returns a tuple of (user, None) or raises an exception if authentication fails.
242+
"""
243+
auth = get_authorization_header(request).split()
244+
245+
if not auth or auth[0].lower() != b'basic':
246+
return None
247+
248+
if len(auth) == 1:
249+
msg = _('Invalid basic header. No credentials provided.')
250+
raise exceptions.AuthenticationFailed(msg)
251+
elif len(auth) > 2:
252+
msg = _('Invalid basic header. Credentials string should not contain spaces.')
253+
raise exceptions.AuthenticationFailed(msg)
254+
255+
try:
256+
try:
257+
auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
258+
except UnicodeDecodeError:
259+
auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
260+
261+
userid, password = auth_decoded.split(':', 1)
262+
except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
263+
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
264+
raise exceptions.AuthenticationFailed(msg)
265+
266+
return self.authenticate_credentials(userid, password, request)
267+
268+
def authenticate_credentials(self, userid, password, request=None):
269+
"""
270+
Authenticate credentials for multiple user models.
271+
"""
272+
# List of user models to authenticate against
273+
user_models = ['users.User', 'admins.AdminUser']
274+
275+
for model_name in user_models:
276+
try:
277+
UserModel = get_user_model() # Replace with custom logic for each model
278+
credentials = {UserModel.USERNAME_FIELD: userid, 'password': password}
279+
user = authenticate(request=request, **credentials)
280+
281+
if user:
282+
if not user.is_active:
283+
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
284+
return (user, None)
285+
except Exception as e:
286+
# Continue to next user model if the current one fails
287+
continue
288+
289+
raise exceptions.AuthenticationFailed(_('Invalid username/password for all user models.'))
290+
291+
def authenticate_header(self, request):
292+
return 'Basic realm="api"'

rest_framework/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
'rest_framework.parsers.FormParser',
3838
'rest_framework.parsers.MultiPartParser'
3939
],
40-
'DEFAULT_AUTHENTICATION_CLASSES': [
40+
'DEFAULT_AUTHENTICATION_CLASSES': [
41+
'rest_framework.authentication.MultiUserModelAuthentication',
4142
'rest_framework.authentication.SessionAuthentication',
4243
'rest_framework.authentication.BasicAuthentication'
4344
],

tests/authentication/test_authentication.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
from django.http import HttpResponse
77
from django.test import TestCase, override_settings
88
from django.urls import include, path
9-
9+
from rest_framework.test import APIClient
10+
from django.test import TestCase
11+
from users.models import User
12+
from admins.models import AdminUser
1013
from rest_framework import (
1114
HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status
1215
)
@@ -597,3 +600,20 @@ def test_remote_user_works(self):
597600
response = self.client.post('/remote-user/',
598601
REMOTE_USER=self.username)
599602
self.assertEqual(response.status_code, status.HTTP_200_OK)
603+
class MultiUserModelAuthenticationTest(TestCase):
604+
def setUp(self):
605+
self.client = APIClient()
606+
self.user = User.objects.create_user(username='user', password='userpass')
607+
self.admin = AdminUser.objects.create_user(username='admin', password='adminpass')
608+
609+
def test_user_authentication(self):
610+
response = self.client.post('/api/token/', {'username': 'user', 'password': 'userpass'})
611+
self.assertEqual(response.status_code, 200)
612+
613+
def test_admin_authentication(self):
614+
response = self.client.post('/api/token/', {'username': 'admin', 'password': 'adminpass'})
615+
self.assertEqual(response.status_code, 200)
616+
617+
def test_invalid_authentication(self):
618+
response = self.client.post('/api/token/', {'username': 'invalid', 'password': 'invalid'})
619+
self.assertEqual(response.status_code, 401)

0 commit comments

Comments
 (0)