Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
crashed with `AttributeError` when a handler returned `AsyncIterable[ServerSentEvent]`
(or any `collections.abc` async-iterable generic). `_try_get_schema_for_generic` now
skips types whose origin has no `__parameters__`.
- Fix [#643](https://github.com/Neoteroi/BlackSheep/issues/643): `use_authorization()`
no longer overrides exception handlers that the user has already registered for
`UnauthorizedError`, `ForbiddenError`, `AuthenticateChallenge`, or
`RateLimitExceededError`. Framework defaults are now installed via `setdefault`
so user-defined handlers win regardless of registration order.

## [2.6.2] - 2026-02-25 :gift:

Expand Down
20 changes: 12 additions & 8 deletions blacksheep/server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,14 +528,18 @@ def use_authorization(
strategy.add(Policy("authenticated", AuthenticatedRequirement()))

self._authorization_strategy = strategy
self.exceptions_handlers.update(
{ # type: ignore
AuthenticateChallenge: handle_authentication_challenge,
UnauthorizedError: handle_unauthorized,
ForbiddenError: handle_forbidden,
RateLimitExceededError: handle_rate_limited_auth,
}
)
# Install framework defaults for authorization exceptions, but only for
# types that the user has not already registered a handler for. This
# lets users register their own handlers either before or after the
# call to ``use_authorization()`` and keep them in effect — see #643.
framework_defaults = { # type: ignore
AuthenticateChallenge: handle_authentication_challenge,
UnauthorizedError: handle_unauthorized,
ForbiddenError: handle_forbidden,
RateLimitExceededError: handle_rate_limited_auth,
}
for exception_type, default_handler in framework_defaults.items():
self.exceptions_handlers.setdefault(exception_type, default_handler)

self.middlewares.append(
get_authorization_middleware(strategy), MiddlewareCategory.AUTHZ, 0
Expand Down
120 changes: 120 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
import pytest
from essentials.secrets import Secret
from guardpost import AuthorizationContext, Identity, Policy, UnauthorizedError
from guardpost.authorization import ForbiddenError
from guardpost.common import AuthenticatedRequirement
from guardpost.jwks import JWKS, InMemoryKeysProvider, KeysProvider
from guardpost.jwts import SymmetricJWTValidator
from pytest import raises
from rodi import Container

from blacksheep import Response, TextContent
from blacksheep.messages import Request
from blacksheep.server.application import Application
from blacksheep.server.authentication import (
AuthenticateChallenge,
AuthenticationHandler,
handle_authentication_challenge,
)
from blacksheep.server.authentication.jwt import JWTBearerAuthentication
from blacksheep.server.authorization import (
Expand All @@ -24,6 +27,8 @@
allow_anonymous,
auth,
get_www_authenticated_header_from_generic_unauthorized_error,
handle_forbidden,
handle_unauthorized,
)
from blacksheep.server.di import di_scope_middleware, register_http_context
from blacksheep.server.resources import get_resource_file_path
Expand Down Expand Up @@ -1138,3 +1143,118 @@ async def home(request):


# endregion


# region user-defined exception handlers vs framework defaults — issue #643


async def test_user_unauthorized_handler_wins_when_registered_before_use_authorization(
app,
):
"""Regression for #643.

Registering an exception handler for ``UnauthorizedError`` BEFORE
``use_authorization()`` must not be silently overridden by the framework
defaults installed inside ``use_authorization()``.
"""

@app.exception_handler(UnauthorizedError)
async def custom_unauthorized(self, request, exc):
return Response(401, content=TextContent("Custom unauthorized page"))

app.use_authentication().add(MockNotAuthHandler())
app.use_authorization().default_policy += AuthenticatedRequirement()

@auth()
@app.router.get("/")
async def home():
return None

await app(get_example_scope("GET", "/"), MockReceive(), MockSend())

assert app.response is not None
assert app.response.status == 401
body = await app.response.text()
assert body == "Custom unauthorized page"


async def test_user_forbidden_handler_wins_when_registered_before_use_authorization(
app,
):
"""Same as above but for ``ForbiddenError`` (HTTP 403)."""

@app.exception_handler(ForbiddenError)
async def custom_forbidden(self, request, exc):
return Response(403, content=TextContent("Custom forbidden page"))

admin = Identity({"id": "001", "role": "user"}, "JWT")
app.use_authentication().add(MockAuthHandler(admin))
app.use_authorization().add(AdminsPolicy())

@auth("admin")
@app.router.get("/")
async def home():
return None

await app(get_example_scope("GET", "/"), MockReceive(), MockSend())

assert app.response is not None
assert app.response.status == 403
body = await app.response.text()
assert body == "Custom forbidden page"


async def test_user_unauthorized_handler_wins_when_registered_after_use_authorization(
app,
):
"""Sanity: registering AFTER ``use_authorization()`` already worked before
#643. This test guards the existing behaviour."""
app.use_authentication().add(MockNotAuthHandler())
app.use_authorization().default_policy += AuthenticatedRequirement()

@app.exception_handler(UnauthorizedError)
async def custom_unauthorized(self, request, exc):
return Response(401, content=TextContent("Custom unauthorized page"))

@auth()
@app.router.get("/")
async def home():
return None

await app(get_example_scope("GET", "/"), MockReceive(), MockSend())

assert app.response is not None
assert app.response.status == 401
body = await app.response.text()
assert body == "Custom unauthorized page"


async def test_framework_defaults_still_apply_when_no_user_handler(app):
"""Sanity: without a user-defined handler, the framework defaults installed
by ``use_authorization()`` must remain in effect."""
app.use_authentication().add(MockNotAuthHandler())
app.use_authorization().default_policy += AuthenticatedRequirement()

@auth()
@app.router.get("/")
async def home():
return None

await app(get_example_scope("GET", "/"), MockReceive(), MockSend())

assert app.response is not None
assert app.response.status == 401
# Framework default body for handle_unauthorized
body = await app.response.text()
assert body == "Unauthorized"

# And the registered handler is the framework's default
assert app.exceptions_handlers[UnauthorizedError] is handle_unauthorized
assert app.exceptions_handlers[ForbiddenError] is handle_forbidden
assert (
app.exceptions_handlers[AuthenticateChallenge]
is handle_authentication_challenge
)


# endregion
Loading