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.d/69.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
**The deprecated header-only `SessionMiddleware` is removed.** It was
deprecated in favour of `FastAPICacheXSessionMiddleware` since 0.3.1, which
reads the same custom header and `Authorization: Bearer` token and adds
`request.session` and the session cookie. Replace
`app.add_middleware(SessionMiddleware, ...)` with
`app.add_middleware(FastAPICacheXSessionMiddleware, ...)` and set the cookie
options (see the 0.4.0 migration guide).
4 changes: 2 additions & 2 deletions docs/HTTP_CACHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ A response that belongs to one caller is never stored either (#296):
backend anyway, but its response to such a request still gets `private`
(before 0.3.9 it was sent without it, #362); `private=True` routes send it
already.
A request has a session when `FastAPICacheXSessionMiddleware` (or the
deprecated `SessionMiddleware`) loaded one for it, from the token header, a
A request has a session when `FastAPICacheXSessionMiddleware` loaded one
for it, from the token header, a
bearer token or the session cookie, with or without a user, or when
`request.session` is non-empty under any session middleware, Starlette's
included. A token that resolves to no session (forged, expired) does not
Expand Down
49 changes: 21 additions & 28 deletions docs/SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,19 @@ FastAPI-CacheX Session Management provides complete user session handling, inclu
tokens, sliding expiration, and optional IP/User-Agent binding. Session contents always live in
the cache backend; the client only holds a single signed token.

**How the token travels depends on which middleware you install:**
**How the token travels with `FastAPICacheXSessionMiddleware`:**

| Middleware | Token source | Response side | Status |
|------------|--------------|---------------|--------|
| `FastAPICacheXSessionMiddleware` | Custom header (default `X-Session-Token`) / `Authorization: Bearer` / **cookie** (default name `session`) | Routed by source: a request that sent a header or bearer token (even one that no longer resolves) gets its token in the response header; otherwise (a cookie, or no token at all) it gets `Set-Cookie` | **Recommended** |
| `SessionMiddleware` | Custom header / `Authorization: Bearer`; **no cookie support** | A renewed token, or one for a regenerated ID, is sent back in the response header | Deprecated, **removed in 0.4.0** |
| Token source | Response side |
|--------------|---------------|
| Custom header (default `X-Session-Token`) / `Authorization: Bearer` / **cookie** (default name `session`) | Routed by source: a request that sent a header or bearer token (even one that no longer resolves) gets its token in the response header; otherwise (a cookie, or no token at all) it gets `Set-Cookie` |

**Use `FastAPICacheXSessionMiddleware` for all new projects.** It covers every transport of
`SessionMiddleware` (it reads `X-Session-Token` and `Authorization: Bearer` in the same way) and
adds cookie support. Since 0.3.1, `SessionMiddleware` emits a `DeprecationWarning` when it is
constructed, and it will be **removed in 0.4.0**. Both middlewares feed the same session
dependencies (`get_session`, `get_optional_session`, `require_session`), so migrating usually only
means changing the `add_middleware` line; existing clients that send the token in a header need no
changes.
The header-only `SessionMiddleware`, deprecated since 0.3.1, was **removed in 0.4.0**; see
[Migration](#migration-sessionmiddleware-fastapicachexsessionmiddleware).

The six `cookie_*` settings of `SessionConfig` (`cookie_name`, `cookie_max_age`, `cookie_path`,
`cookie_same_site`, `cookie_https_only`, `cookie_domain`) are **read only by
`FastAPICacheXSessionMiddleware`**; setting them has no effect when `SessionMiddleware` is installed.
`FastAPICacheXSessionMiddleware`**; setting them has no effect when `SessionManager` is used
without it.

Complete runnable examples: [`examples/session_login.py`](https://github.com/allen0099/FastAPI-CacheX/blob/master/examples/session_login.py) and [`examples/session_jwt.py`](https://github.com/allen0099/FastAPI-CacheX/blob/master/examples/session_jwt.py).

Expand Down Expand Up @@ -136,12 +131,12 @@ Both methods delete what they find with a single `backend.delete_many()` call.

## Migration: SessionMiddleware → FastAPICacheXSessionMiddleware

`SessionMiddleware` has been deprecated since 0.3.1 (it emits a `DeprecationWarning` when
constructed) and will be removed in 0.4.0. Use `FastAPICacheXSessionMiddleware` instead:
`SessionMiddleware`, deprecated since 0.3.1, was removed in 0.4.0. Use
`FastAPICacheXSessionMiddleware` instead:

- **`SessionMiddleware`** (a `BaseHTTPMiddleware`): passes the token in a custom header (default
`X-Session-Token`) and/or `Authorization: Bearer`, suited to API-first architectures where the
client manages the token. Cookie transport is not supported.
- **`SessionMiddleware`** (a `BaseHTTPMiddleware`, removed): passed the token in a custom header
(default `X-Session-Token`) and/or `Authorization: Bearer`, suited to API-first architectures
where the client manages the token. Cookie transport was not supported.
- **`FastAPICacheXSessionMiddleware`** (a pure ASGI middleware): compatible with Starlette's
built-in `SessionMiddleware`, exposing the same dict-like `request.session`. It passes the signed
session token in a cookie (default cookie name `session`), while the session contents are stored
Expand All @@ -155,9 +150,9 @@ constructed) and will be removed in 0.4.0. Use `FastAPICacheXSessionMiddleware`
`Set-Cookie` is emitted; a token that arrived in a cookie (or a brand-new anonymous session for a
request without a token) uses `Set-Cookie`.

Both middlewares put the loaded `Session` object into `request.state`, so the existing session
dependencies `get_session`, `get_optional_session`, `require_session` and `require_user_session`
work under either middleware without any changes:
`FastAPICacheXSessionMiddleware` puts the loaded `Session` object into `request.state` as
`SessionMiddleware` did, so the session dependencies `get_session`, `get_optional_session`,
`require_session` and `require_user_session` work without any changes:

```python
from fastapi import Depends
Expand Down Expand Up @@ -205,8 +200,7 @@ async def me(session=Depends(require_user_session)):
`Cache-Control: private, no-store`, replacing whatever the route set (a `@cache(public=True)`
route included), and adds the same `Vary` names as above even when the handler never touched
`request.session`. Otherwise a CDN or reverse proxy could store the token and hand it to the
next visitor. Responses without a token keep their headers. The deprecated `SessionMiddleware`
does the same when it sends a token in its response header.
next visitor. Responses without a token keep their headers.
- `@cache` does not read or write its backend for a request that arrived with a session (one
the middleware loaded, from any transport, with or without a user, or a non-empty
`request.session`), and answers it with `private`, as for `Authorization`. `public=True`
Expand Down Expand Up @@ -293,8 +287,7 @@ token's source (header in, header out; cookie in, `Set-Cookie` out).

Until 0.4.0 the cookie is read whether or not the list names it. `"cookie"` is accepted only as the
last entry, which is where it is read anyway, so listing it changes nothing yet; any other position
raises a `ValidationError`. The deprecated `SessionMiddleware` never reads the cookie and ignores
the entry.
raises a `ValidationError`.

In 0.4.0 the list names every token source, and its default becomes `["header", "bearer",
"cookie"]`, the order used today. A list without `"cookie"` then means no cookie at all: the
Expand Down Expand Up @@ -525,9 +518,9 @@ session `login()` returned, or issue the token from a separate endpoint, as
Within one request, `request.session.clear()` after `login()` is a logout: the new session is
deleted and no token is sent (a cookie client gets its cookie expired). `clear()` before
`login()` logs the loaded session out, and `login()` then starts a new session instead of
rotating it. Without `FastAPICacheXSessionMiddleware`, `login()` raises `RuntimeError`: the
deprecated `SessionMiddleware` cannot send a token for a session it did not load, so there
create the session with `create_session(user=...)` and return its token.
rotating it. Without `FastAPICacheXSessionMiddleware`, `login()` raises `RuntimeError`, because
nothing would send the token; create the session with `create_session(user=...)` and return its
token instead.

`request.session["user_id"] = "123"` is not a login. It is application data, which
`require_user_session` and `AuthenticatedSession` do not recognise, and it keeps the session ID
Expand Down
2 changes: 0 additions & 2 deletions docs/api/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ See the [session management guide](../SESSION.md) for how the pieces fit togethe
::: fastapi_cachex.session.dependencies

::: fastapi_cachex.session.exceptions

::: fastapi_cachex.session.middleware.SessionMiddleware
2 changes: 0 additions & 2 deletions fastapi_cachex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from .session import SessionConfig as SessionConfig
from .session import SessionManager as SessionManager
from .session import SessionManagerProxy as SessionManagerProxy
from .session import SessionMiddleware as SessionMiddleware
from .session import SessionUser as SessionUser
from .session import get_optional_session as get_optional_session
from .session import get_session as get_session
Expand Down Expand Up @@ -94,7 +93,6 @@ def _read_version() -> str:
"SessionInvalidError",
"SessionManager",
"SessionManagerProxy",
"SessionMiddleware",
"SessionNotFoundError",
"SessionSecurityError",
"SessionTokenError",
Expand Down
4 changes: 2 additions & 2 deletions fastapi_cachex/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ def _vary_components(request: Request, names: Sequence[str]) -> list[str]:
return components


# Where `FastAPICacheXSessionMiddleware` (and the deprecated
# `SessionMiddleware`) put the session they loaded; `get_session` reads it.
# Where `FastAPICacheXSessionMiddleware` puts the session it loaded;
# `get_session` reads it.
_SESSION_STATE_KEY = "__fastapi_cachex_session"


Expand Down
2 changes: 0 additions & 2 deletions fastapi_cachex/session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from .dependencies import rotate_session_id
from .manager import SessionManager
from .middleware import FastAPICacheXSessionMiddleware
from .middleware import SessionMiddleware
from .middleware import get_client_ip
from .models import Session
from .models import SessionUser
Expand All @@ -23,7 +22,6 @@
"SessionConfig",
"SessionManager",
"SessionManagerProxy",
"SessionMiddleware",
"SessionUser",
"get_client_ip",
"get_optional_session",
Expand Down
10 changes: 4 additions & 6 deletions fastapi_cachex/session/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,7 @@ async def rotate_session_id(request: Request) -> bool:
``SessionUser`` that ``require_user_session`` / ``AuthenticatedSession``
check, and makes the middleware save the session and send its token,
also for a visitor who had no session yet. Use this function on its own
when the ID should change without a login (a privilege change, say), or
under the deprecated ``SessionMiddleware``.
when the ID should change without a login (a privilege change, say).

A session token the client arrived with may have been planted by someone
else; after rotation the old token no longer resolves, and the middleware
Expand Down Expand Up @@ -322,10 +321,9 @@ async def log_in(credentials: Credentials, request: Request):

Raises:
RuntimeError: If the request did not pass through
``FastAPICacheXSessionMiddleware``. The deprecated
``SessionMiddleware`` cannot send a token for a session it did not
load; there, create the session with
``SessionManager.create_session(user=...)`` and return its token.
``FastAPICacheXSessionMiddleware``. Without it, create the
session with ``SessionManager.create_session(user=...)`` and
return its token.
"""
request_session = request.scope.get("session")
if (
Expand Down
168 changes: 2 additions & 166 deletions fastapi_cachex/session/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
from typing import TYPE_CHECKING
from typing import Any

from fastapi import Request
from fastapi import Response
from starlette.datastructures import MutableHeaders
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.base import RequestResponseEndpoint
from starlette.middleware.sessions import Session as StarletteSession
from starlette.requests import HTTPConnection
from starlette.types import ASGIApp
Expand Down Expand Up @@ -85,26 +81,6 @@ def get_client_ip(connection: HTTPConnection, config: SessionConfig) -> str | No
return peer


def _extract_header_token(
connection: HTTPConnection, config: SessionConfig
) -> str | None:
"""Extract a session token from request headers.

Honours ``SessionConfig.token_source_priority``: checks the configured
custom header (``config.header_name``) and/or an ``Authorization: Bearer``
token. This is the header/bearer transport shared with ``SessionMiddleware``.

Args:
connection: Incoming HTTP connection (or a `Request`, which IS-A
`HTTPConnection`)
config: Session configuration

Returns:
Session token or None
"""
return _read_header_token(connection, config)[0]


def _read_header_token(
connection: HTTPConnection, config: SessionConfig
) -> tuple[str | None, list[str]]:
Expand Down Expand Up @@ -249,146 +225,6 @@ def _warn_if_priority_without_cookie(config: SessionConfig) -> None:
)


class SessionMiddleware(BaseHTTPMiddleware):
"""Middleware to handle session loading and token extraction.

Extracts the session token from the request (via a custom header and/or
an ``Authorization: Bearer`` header, per ``SessionConfig.token_source_priority``)
and loads the corresponding session into ``request.state``. Cookie-based
token transport is not supported, so a ``"cookie"`` entry in the list is
ignored.

.. deprecated:: 0.3.1
Use :class:`FastAPICacheXSessionMiddleware` instead. Will be removed in
version 0.4.0.
"""

def __init__(
self,
app: ASGIApp,
session_manager: SessionManager | None = None,
config: SessionConfig | None = None,
) -> None:
"""Initialize session middleware.

Args:
app: ASGI application
session_manager: Session manager instance; defaults to the one
set in ``SessionManagerProxy``
config: Session configuration; defaults to
``session_manager.config``

Raises:
ProxyNotSetError: If ``session_manager`` is omitted and
``SessionManagerProxy`` holds none.

Warns:
DeprecationWarning: Always; use ``FastAPICacheXSessionMiddleware``.
"""
warnings.warn(
"SessionMiddleware is deprecated, use FastAPICacheXSessionMiddleware. "
"Will be removed in version 0.4.0.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(app)
self.session_manager = session_manager or SessionManagerProxy.get()

if config is None:
config = self.session_manager.config

self.config = config

logger.debug(
"SessionMiddleware initialized; header=%s bearer=%s",
config.header_name,
config.use_bearer_token,
)

async def dispatch(
self,
request: Request,
call_next: RequestResponseEndpoint,
) -> Response:
"""Process request and handle session.

Args:
request: Incoming request
call_next: Next handler in chain

Returns:
Response
"""
_stash_session_manager(request.app, self.session_manager)

# Extract session token from request
token = self._extract_token(request)

# Try to load session
session: Session | None = None
renewed_token: str | None = None
if token:
try:
ip_address = self._get_client_ip(request)
user_agent = request.headers.get("user-agent")
session, renewed_token = await self.session_manager.get_session(
token,
ip_address=ip_address,
user_agent=user_agent,
)
logger.debug("Session loaded in middleware; id=%s", session.session_id)
except SessionError:
# Session invalid/expired, continue without session
session = None
logger.debug("Session failed to load; token invalid/expired")

# Store session in request state
setattr(request.state, "__fastapi_cachex_session", session)

loaded_session_id = session.session_id if session is not None else None

# Process request
response: Response = await call_next(request)

if session is not None and session.session_id != loaded_session_id:
# The handler regenerated the session ID; a renewed token would
# name the deleted record, so send a token for the new ID.
response_token: str | None = self.session_manager.issue_token(session)
else:
# Propagate renewed token to client so its JWT exp stays in sync
response_token = renewed_token

if response_token is not None or _session_was_read(request):
add_vary(response.headers, _read_header_token(request, self.config)[1])
if response_token is not None:
response.headers[self.config.header_name] = response_token
_forbid_storing(response.headers)

return response

def _extract_token(self, request: Request) -> str | None:
"""Extract session token from request.

Args:
request: Incoming request

Returns:
Session token or None
"""
return _extract_header_token(request, self.config)

def _get_client_ip(self, request: Request) -> str | None:
"""Get client IP address from request.

Args:
request: Incoming request

Returns:
Client IP address or None
"""
return get_client_ip(request, self.config)


class _RequestSession(StarletteSession):
"""``request.session`` that remembers an explicit ``clear()``.

Expand Down Expand Up @@ -489,8 +325,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
loaded_session_id: str | None = None

# Resolve the incoming session token: prefer the header/bearer transport
# (e.g. X-Session-Token, as used by SessionMiddleware) and fall back to
# the session cookie, so header-based clients authenticate here too.
# (e.g. X-Session-Token) and fall back to the session cookie, so
# header-based clients authenticate here too.
# `header_token` is captured so the response is routed by transport: a
# header-sourced token is echoed back via the response header, otherwise
# via Set-Cookie (see send_wrapper).
Expand Down
Loading
Loading