diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d213524c..738927c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] ### Added * Add migration to include `token_checksum` field in AbstractAccessToken model. +* Added compatibility with `LoginRequiredMiddleware` introduced in Django 5.1 * #1404 Add a new setting `REFRESH_TOKEN_REUSE_PROTECTION` ### Changed * Update token to TextField from CharField with 255 character limit and SHA-256 checksum in AbstractAccessToken model. Removing the 255 character limit enables supporting JWT tokens with additional claims diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index 0c83cb37a..846e32d0e 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -2,3 +2,14 @@ The `compat` module provides support for backwards compatibility with older versions of Django and Python. """ + +try: + # Django 5.1 introduced LoginRequiredMiddleware, and login_not_required decorator + from django.contrib.auth.decorators import login_not_required +except ImportError: + + def login_not_required(view_func): + return view_func + + +__all__ = ["login_not_required"] diff --git a/oauth2_provider/views/base.py b/oauth2_provider/views/base.py index 52cb151d5..d2644f35f 100644 --- a/oauth2_provider/views/base.py +++ b/oauth2_provider/views/base.py @@ -13,6 +13,7 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import FormView, View +from ..compat import login_not_required from ..exceptions import OAuthToolkitError from ..forms import AllowForm from ..http import OAuth2ResponseRedirect @@ -26,6 +27,8 @@ log = logging.getLogger("oauth2_provider") +# login_not_required decorator to bypass LoginRequiredMiddleware +@method_decorator(login_not_required, name="dispatch") class BaseAuthorizationView(LoginRequiredMixin, OAuthLibMixin, View): """ Implements a generic endpoint to handle *Authorization Requests* as in :rfc:`4.1.1`. The view @@ -274,6 +277,7 @@ def handle_no_permission(self): @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class TokenView(OAuthLibMixin, View): """ Implements an endpoint to provide access tokens @@ -301,6 +305,7 @@ def post(self, request, *args, **kwargs): @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class RevokeTokenView(OAuthLibMixin, View): """ Implements an endpoint to revoke access or refresh tokens diff --git a/oauth2_provider/views/introspect.py b/oauth2_provider/views/introspect.py index 05a77909f..5474c3a7e 100644 --- a/oauth2_provider/views/introspect.py +++ b/oauth2_provider/views/introspect.py @@ -6,11 +6,13 @@ from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt -from oauth2_provider.models import get_access_token_model -from oauth2_provider.views.generic import ClientProtectedScopedResourceView +from ..compat import login_not_required +from ..models import get_access_token_model +from ..views.generic import ClientProtectedScopedResourceView @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class IntrospectTokenView(ClientProtectedScopedResourceView): """ Implements an endpoint for token introspection based diff --git a/oauth2_provider/views/oidc.py b/oauth2_provider/views/oidc.py index c9d10c25e..c746c30ce 100644 --- a/oauth2_provider/views/oidc.py +++ b/oauth2_provider/views/oidc.py @@ -14,6 +14,7 @@ from jwcrypto.jwt import JWTExpired from oauthlib.common import add_params_to_uri +from ..compat import login_not_required from ..exceptions import ( ClientIdMissmatch, InvalidIDTokenError, @@ -39,6 +40,7 @@ Application = get_application_model() +@method_decorator(login_not_required, name="dispatch") class ConnectDiscoveryInfoView(OIDCOnlyMixin, View): """ View used to show oidc provider configuration information per @@ -106,6 +108,7 @@ def get(self, request, *args, **kwargs): return response +@method_decorator(login_not_required, name="dispatch") class JwksInfoView(OIDCOnlyMixin, View): """ View used to show oidc json web key set document @@ -134,6 +137,7 @@ def get(self, request, *args, **kwargs): @method_decorator(csrf_exempt, name="dispatch") +@method_decorator(login_not_required, name="dispatch") class UserInfoView(OIDCOnlyMixin, OAuthLibMixin, View): """ View used to show Claims about the authenticated End-User @@ -211,6 +215,7 @@ def _validate_claims(request, claims): return True +@method_decorator(login_not_required, name="dispatch") class RPInitiatedLogoutView(OIDCLogoutOnlyMixin, FormView): template_name = "oauth2_provider/logout_confirm.html" form_class = ConfirmLogoutForm diff --git a/tests/conftest.py b/tests/conftest.py index eff48f7fb..2510025ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from urllib.parse import parse_qs, urlparse import pytest +from django import VERSION from django.conf import settings as test_settings from django.contrib.auth import get_user_model from django.urls import reverse @@ -294,3 +295,13 @@ def oidc_non_confidential_tokens(oauth2_settings, public_application, test_user, "openid", "http://other.org", ) + + +@pytest.fixture(autouse=True) +def django_login_required_middleware(settings, request): + if "nologinrequiredmiddleware" in request.keywords: + return + + # Django 5.1 introduced LoginRequiredMiddleware + if VERSION[0] >= 5 and VERSION[1] >= 1: + settings.MIDDLEWARE = [*settings.MIDDLEWARE, "django.contrib.auth.middleware.LoginRequiredMiddleware"] diff --git a/tests/test_introspection_auth.py b/tests/test_introspection_auth.py index 100ef064e..d96a013e3 100644 --- a/tests/test_introspection_auth.py +++ b/tests/test_introspection_auth.py @@ -11,6 +11,7 @@ from django.utils import timezone from oauthlib.common import Request +from oauth2_provider.compat import login_not_required from oauth2_provider.models import get_access_token_model, get_application_model from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.settings import oauth2_settings @@ -93,7 +94,7 @@ def mocked_introspect_request_short_living_token(url, data, *args, **kwargs): urlpatterns = [ path("oauth2/", include("oauth2_provider.urls")), - path("oauth2-test-resource/", ScopeResourceView.as_view()), + path("oauth2-test-resource/", login_not_required(ScopeResourceView.as_view())), ] diff --git a/tests/test_rest_framework.py b/tests/test_rest_framework.py index 632c62e26..84b4ad7d9 100644 --- a/tests/test_rest_framework.py +++ b/tests/test_rest_framework.py @@ -127,6 +127,7 @@ class AuthenticationNoneOAuth2View(MockView): @override_settings(ROOT_URLCONF=__name__) +@pytest.mark.nologinrequiredmiddleware @pytest.mark.usefixtures("oauth2_settings") @pytest.mark.oauth2_settings(presets.REST_FRAMEWORK_SCOPES) class TestOAuth2Authentication(TestCase): diff --git a/tox.ini b/tox.ini index 56d249661..42819568f 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,7 @@ addopts = -s markers = oauth2_settings: Custom OAuth2 settings to use - use with oauth2_settings fixture + nologinrequiredmiddleware [testenv] commands =