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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ Note that 0.3.3 was never released; 0.3.4 follows 0.3.2.
are no longer read; `clear()` removes them.
([#110](https://github.com/allen0099/FastAPI-CacheX/issues/110))

- **The session middleware reads `Authorization: bearer <token>` in any
letter case.** Authentication scheme names are case-insensitive (RFC 9110
§11.1), but only the exact `Bearer ` prefix was recognised, so a client
sending `bearer` got no session. More than one space before the token is
accepted too, and a header without a token no longer yields an empty one.
([#166](https://github.com/allen0099/FastAPI-CacheX/issues/166))

## [0.3.7] - 2026-09-25

### Added
Expand Down
11 changes: 7 additions & 4 deletions fastapi_cachex/session/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ def _extract_header_token(

elif source == "bearer":
if config.use_bearer_token:
auth_header = connection.headers.get("authorization")
if auth_header and auth_header.startswith("Bearer "):
bearer_prefix_len = 7
token_value = auth_header[bearer_prefix_len:]
# The scheme name is case-insensitive (RFC 9110 §11.1) and is
# followed by one or more spaces (RFC 6750 §2.1).
scheme, _, token_value = connection.headers.get(
"authorization", ""
).partition(" ")
token_value = token_value.lstrip(" ")
if scheme.lower() == "bearer" and token_value:
logger.debug("Token extracted from bearer auth")
return token_value

Expand Down
32 changes: 32 additions & 0 deletions tests/session/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,38 @@ def test_bearer_is_used_when_the_header_source_finds_nothing(
assert token == "from-bearer"


@pytest.mark.parametrize(
"authorization",
[
"Bearer from-bearer",
"bearer from-bearer",
"BEARER from-bearer",
"Bearer from-bearer",
],
)
def test_bearer_scheme_is_matched_case_insensitively(
config: SessionConfig, authorization: str
) -> None:
"""Auth schemes are case-insensitive (RFC 9110 §11.1), and RFC 6750 allows
more than one space before the token (#166).
"""
token = _extract_header_token(_connection({"Authorization": authorization}), config)

assert token == "from-bearer"


@pytest.mark.parametrize(
"authorization",
["Bearer", "Bearer ", "Bearer ", "Basic from-bearer", "Bearerfrom-bearer"],
)
def test_an_empty_or_non_bearer_authorization_header_yields_no_token(
config: SessionConfig, authorization: str
) -> None:
token = _extract_header_token(_connection({"Authorization": authorization}), config)

assert token is None


def test_header_wins_over_bearer_when_both_are_present(
config: SessionConfig,
) -> None:
Expand Down
Loading