From 8ffc65c9a3f0bc3b384cfbf22c03ccb1cd9653d1 Mon Sep 17 00:00:00 2001 From: Larry-Osakwe Date: Thu, 30 Apr 2026 11:23:00 -0700 Subject: [PATCH] refactor(keycardai-starlette)!: remove deprecated BearerAuthMiddleware shims (ACC-237) PR #97 kept BearerAuthMiddleware, verify_bearer_token, and _create_auth_challenge_response in keycardai.starlette.middleware.bearer as deprecated shims for keycardai-mcp and keycardai-agents to migrate off. keycardai-mcp moved to AuthenticationMiddleware + KeycardAuthBackend in ACC-235; keycardai-agents archived in ACC-232. Both consumers are off the shim, so it is removed. Removes: - BearerAuthMiddleware - verify_bearer_token - _create_auth_challenge_response - packages/starlette/tests/keycardai/starlette/test_deprecation_warnings.py - BearerAuthMiddleware and verify_bearer_token from middleware/__init__.py __all__ - Now-unused imports: warnings, Callable, BaseHTTPMiddleware, ASGIApp BREAKING: any "from keycardai.starlette import BearerAuthMiddleware" or "verify_bearer_token" import fails. The runtime DeprecationWarning has been shipping since 0.3.0. Closes ACC-237. --- .../starlette/middleware/__init__.py | 4 - .../keycardai/starlette/middleware/bearer.py | 173 +----------------- .../starlette/test_deprecation_warnings.py | 105 ----------- 3 files changed, 7 insertions(+), 275 deletions(-) delete mode 100644 packages/starlette/tests/keycardai/starlette/test_deprecation_warnings.py diff --git a/packages/starlette/src/keycardai/starlette/middleware/__init__.py b/packages/starlette/src/keycardai/starlette/middleware/__init__.py index cb071c5..76f8798 100644 --- a/packages/starlette/src/keycardai/starlette/middleware/__init__.py +++ b/packages/starlette/src/keycardai/starlette/middleware/__init__.py @@ -1,19 +1,15 @@ from .bearer import ( - BearerAuthMiddleware, KeycardAuthBackend, KeycardAuthCredentials, KeycardAuthError, KeycardUser, keycard_on_error, - verify_bearer_token, ) __all__ = [ - "BearerAuthMiddleware", "KeycardAuthBackend", "KeycardAuthCredentials", "KeycardAuthError", "KeycardUser", "keycard_on_error", - "verify_bearer_token", ] diff --git a/packages/starlette/src/keycardai/starlette/middleware/bearer.py b/packages/starlette/src/keycardai/starlette/middleware/bearer.py index 7cdd307..9862fc0 100644 --- a/packages/starlette/src/keycardai/starlette/middleware/bearer.py +++ b/packages/starlette/src/keycardai/starlette/middleware/bearer.py @@ -1,28 +1,16 @@ """Standard Starlette authentication backend for Keycard bearer tokens. -This module exposes two layers: - -1. The current API (used by ``AuthProvider.install``): - ``KeycardAuthBackend`` (a standard ``AuthenticationBackend``) that verifies - incoming bearer tokens via a ``TokenVerifier`` and populates - ``request.user`` (a ``KeycardUser``) and ``request.auth`` (a - ``KeycardAuthCredentials``). The on-error hook (``keycard_on_error``) - maps a ``KeycardAuthError`` raised by the backend into an RFC 6750 - ``WWW-Authenticate`` challenge that includes the ``resource_metadata=`` - URL required by RFC 9728. - -2. Deprecated legacy symbols (``BearerAuthMiddleware``, ``verify_bearer_token``, - ``_create_auth_challenge_response``) preserved for downstream packages - (``keycardai-mcp``, ``keycardai-agents``) until those callers migrate to - ``AuthenticationMiddleware(backend=KeycardAuthBackend(...), - on_error=keycard_on_error)``. They will be removed once the migration - is complete; do not use them in new code. +``KeycardAuthBackend`` (a standard ``AuthenticationBackend``) verifies +incoming bearer tokens via a ``TokenVerifier`` and populates ``request.user`` +(a ``KeycardUser``) and ``request.auth`` (a ``KeycardAuthCredentials``). +The on-error hook (``keycard_on_error``) maps a ``KeycardAuthError`` raised +by the backend into an RFC 6750 ``WWW-Authenticate`` challenge that +includes the ``resource_metadata=`` URL required by RFC 9728. """ from __future__ import annotations -import warnings -from collections.abc import Callable, Sequence +from collections.abc import Sequence from pydantic import AnyHttpUrl @@ -33,10 +21,8 @@ AuthenticationError, BaseUser, ) -from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import HTTPConnection, Request from starlette.responses import Response -from starlette.types import ASGIApp from ..shared.starlette import get_base_url @@ -90,29 +76,6 @@ def _build_challenge_header(error: str, description: str, resource_metadata: str ) -def _create_auth_challenge_response( - error: str, - description: str, - request: Request, - status_code: int = 401, -) -> Response: - """Create a standardized OAuth 2.0 Bearer challenge response (RFC 6750). - - .. deprecated:: - Kept for ``BearerAuthMiddleware`` and downstream callers. New code - should rely on ``keycard_on_error`` together with - ``KeycardAuthBackend``. - """ - response = Response( - content="Unauthorized" if status_code == 401 else "Forbidden", - status_code=status_code, - ) - response.headers["WWW-Authenticate"] = _build_challenge_header( - error, description, _get_oauth_protected_resource_url(request) - ) - return response - - class KeycardAuthError(AuthenticationError): """AuthenticationError carrying the OAuth ``error`` code and HTTP status. @@ -310,125 +273,3 @@ def keycard_on_error(conn: HTTPConnection, exc: Exception) -> Response: conn, description=str(exc) or "Authentication failed", ) - - -# --------------------------------------------------------------------------- -# Deprecated legacy surface -# --------------------------------------------------------------------------- -# Preserved so that ``keycardai-mcp`` and ``keycardai-agents`` continue to -# import and use ``BearerAuthMiddleware`` / ``verify_bearer_token`` while a -# follow-up migrates them to ``KeycardAuthBackend`` + ``AuthenticationMiddleware``. -# Do not use these in new ``keycardai-starlette`` code. - - -async def verify_bearer_token( - request: Request, - verifier: TokenVerifier, - *, - _from_middleware: bool = False, -) -> dict[str, str | None] | Response: - """Verify the request's bearer token. - - Returns an auth_info dict on success (suitable for assigning to - ``request.state.keycardai_auth_info``) or an RFC 6750 challenge - ``Response`` on failure. - - .. deprecated:: - Kept for ``BearerAuthMiddleware`` compatibility. New code should rely - on ``KeycardAuthBackend``. - """ - if not _from_middleware: - warnings.warn( - "verify_bearer_token is deprecated and will be removed in a " - "future release. Use KeycardAuthBackend(verifier) wired to " - "starlette.middleware.authentication.AuthenticationMiddleware; " - "results are exposed via request.user / request.auth.", - DeprecationWarning, - stacklevel=2, - ) - if not request.headers.get("Authorization"): - return _create_auth_challenge_response( - "invalid_token", "No bearer token provided", request - ) - token = _get_bearer_token(request) - if token is None: - return _create_auth_challenge_response( - "invalid_token", - "Invalid Authorization header format", - request, - 400, - ) - - zone_id = None - if verifier.enable_multi_zone: - zone_id = request.path_params.get("zone_id") - if zone_id is None: - return _create_auth_challenge_response( - "invalid_token", "Zone ID is required", request - ) - - if verifier.enable_multi_zone and zone_id: - access_token = await verifier.verify_token_for_zone(token, zone_id) - else: - access_token = await verifier.verify_token(token) - if access_token is None: - return _create_auth_challenge_response( - "invalid_token", "Token verification failed", request - ) - - resource_server_url = _get_oauth_protected_resource_url(request) - return { - "access_token": access_token.token, - "zone_id": zone_id, - "resource_client_id": resource_server_url, - "resource_server_url": resource_server_url, - } - - -class BearerAuthMiddleware(BaseHTTPMiddleware): - """Starlette middleware that validates OAuth 2.0 bearer tokens. - - On success, populates ``request.state.keycardai_auth_info`` with:: - - { - "access_token": "", - "zone_id": "", - "resource_client_id": "", - "resource_server_url": "", - } - - On failure, returns a ``WWW-Authenticate`` challenge per RFC 6750. - - .. deprecated:: - Use ``starlette.middleware.authentication.AuthenticationMiddleware`` - wired to :class:`KeycardAuthBackend` with ``on_error=keycard_on_error``. - This class will be removed once ``keycardai-mcp`` and - ``keycardai-agents`` migrate. - """ - - def __init__(self, app: ASGIApp, verifier: TokenVerifier): - warnings.warn( - "BearerAuthMiddleware is deprecated and will be removed in a " - "future release. Use " - "starlette.middleware.authentication.AuthenticationMiddleware " - "with backend=KeycardAuthBackend(verifier) and " - "on_error=keycard_on_error.", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(app) - self.verifier = verifier - - async def dispatch( - self, request: Request, call_next: Callable - ) -> Response: - if _is_oauth_metadata_path(request.url.path): - return await call_next(request) - - result = await verify_bearer_token( - request, self.verifier, _from_middleware=True - ) - if isinstance(result, Response): - return result - request.state.keycardai_auth_info = result - return await call_next(request) diff --git a/packages/starlette/tests/keycardai/starlette/test_deprecation_warnings.py b/packages/starlette/tests/keycardai/starlette/test_deprecation_warnings.py deleted file mode 100644 index d6ee5c5..0000000 --- a/packages/starlette/tests/keycardai/starlette/test_deprecation_warnings.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Runtime DeprecationWarning tests for the legacy bearer surface. - -The deprecated symbols (`BearerAuthMiddleware`, `verify_bearer_token`) -are retained as shims so `keycardai-mcp` and `keycardai-agents` keep -working until they migrate to `KeycardAuthBackend` + -`AuthenticationMiddleware`. Until those migrations land, downstream -users importing the deprecated symbols should see a runtime warning -pointing at the replacement. -""" - -import warnings -from unittest.mock import AsyncMock, MagicMock - -import pytest -from starlette.applications import Starlette -from starlette.middleware import Middleware -from starlette.responses import PlainTextResponse -from starlette.routing import Route -from starlette.testclient import TestClient - -from keycardai.starlette.middleware.bearer import ( - BearerAuthMiddleware, - verify_bearer_token, -) - - -def _stub_verifier() -> MagicMock: - token = MagicMock(token="verified-token", client_id="test-client", scopes=[]) - return MagicMock( - enable_multi_zone=False, - verify_token=AsyncMock(return_value=token), - verify_token_for_zone=AsyncMock(return_value=token), - ) - - -def test_bearer_auth_middleware_init_warns(): - with pytest.warns( - DeprecationWarning, - match=r"BearerAuthMiddleware.*KeycardAuthBackend", - ): - BearerAuthMiddleware(MagicMock(), _stub_verifier()) - - -@pytest.mark.asyncio -async def test_verify_bearer_token_call_warns(): - """Direct external call to verify_bearer_token fires the warning. - - The warning fires before any request introspection; catch it explicitly - so post-warning failures (the MagicMock request not satisfying pydantic - URL parsing) don't mask the assertion under test. - """ - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - try: - await verify_bearer_token(MagicMock(), _stub_verifier()) - except Exception: - pass - - matches = [ - w - for w in caught - if issubclass(w.category, DeprecationWarning) - and "verify_bearer_token" in str(w.message) - and "KeycardAuthBackend" in str(w.message) - ] - assert len(matches) == 1, f"Expected exactly one warning, got {matches}" - - -def test_middleware_dispatch_does_not_double_warn(): - """A single BearerAuthMiddleware instance must fire exactly one warning. - - The internal `dispatch` path must call `verify_bearer_token` with - `_from_middleware=True` so the per-request flow does not emit a second - warning beyond the one fired at middleware construction. - """ - async def endpoint(request): - return PlainTextResponse("ok") - - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - app = Starlette( - routes=[Route("/api/me", endpoint)], - middleware=[Middleware(BearerAuthMiddleware, verifier=_stub_verifier())], - ) - client = TestClient(app, raise_server_exceptions=False) - client.get("/api/me", headers={"Authorization": "Bearer some-token"}) - - bearer_warnings = [ - w - for w in caught - if issubclass(w.category, DeprecationWarning) - and "BearerAuthMiddleware" in str(w.message) - ] - verify_warnings = [ - w - for w in caught - if issubclass(w.category, DeprecationWarning) - and "verify_bearer_token" in str(w.message) - ] - assert len(bearer_warnings) == 1, ( - f"Expected exactly one BearerAuthMiddleware warning, got {len(bearer_warnings)}" - ) - assert verify_warnings == [], ( - f"verify_bearer_token must not warn when called from dispatch; got {verify_warnings}" - )