Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/api-guide/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

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

Expand Down
Empty file added example/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions example/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for example project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')

application = get_asgi_application()
124 changes: 124 additions & 0 deletions example/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""
Django settings for example project.

Generated by 'django-admin startproject' using Django 4.2.16.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-=o!jcfy@7jrg=_lvp39w%%4-h&620xr985@v!(sn&6uv7%jcs-'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
]

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

ROOT_URLCONF = 'example.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'example.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}


# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]


# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
28 changes: 28 additions & 0 deletions example/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.contrib.auth.models import User
from django.urls import include, path
from rest_framework import routers, serializers, viewsets


# Serializers define the API representation.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'email', 'is_staff']


# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer


# Routers provide a way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
16 changes: 16 additions & 0 deletions example/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for example project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')

application = get_wsgi_application()
60 changes: 60 additions & 0 deletions rest_framework/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,63 @@ def authenticate(self, request):
user = authenticate(request=request, remote_user=request.META.get(self.header))
if user and user.is_active:
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"'
3 changes: 2 additions & 1 deletion rest_framework/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.MultiUserModelAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
Expand Down
22 changes: 21 additions & 1 deletion tests/authentication/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from django.http import HttpResponse
from django.test import TestCase, override_settings
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 (
HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status
)
Expand Down Expand Up @@ -597,3 +600,20 @@ def test_remote_user_works(self):
response = self.client.post('/remote-user/',
REMOTE_USER=self.username)
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)
Loading