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
12 changes: 12 additions & 0 deletions changelog.d/256.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
**The session cookie defaults to `__Host-session` with the `Secure` flag, and
`SessionConfig` rejects cookie settings browsers would refuse.** `SessionConfig.cookie_name`
defaults to `"__Host-session"` and `cookie_https_only` to `True`, so a planted
cookie from a subdomain or over plain HTTP is refused by the browser. The new
name logs every cookie session out once after the upgrade. For plain-HTTP
development, set `cookie_name="session", cookie_https_only=False`. A
`__Host-` name without `Secure`, with a `cookie_path` other than `"/"` or with
a `cookie_domain`, and a `__Secure-` name without `Secure`, raise a
`ValidationError` instead of a `UserWarning`; because of the new default name,
so does changing only one of those settings. The middleware's `FutureWarning`
about the defaults is removed. See the
[migration guide](https://fastapi-cachex.readthedocs.io/en/stable/MIGRATING_0_4/#session-cookie).
4 changes: 2 additions & 2 deletions docs/MIGRATING_0_4.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Every warning below names the setting to change and links to its issue. `FutureW
| Change | Issue | Warned in 0.3.9 | Section |
|--------|-------|-----------------|---------|
| Session cookie defaults to `__Host-session` with `Secure` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `FutureWarning` | [Session cookie](#session-cookie) |
| Contradictory `__Host-` / `__Secure-` cookie settings are rejected | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `UserWarning` | [Session cookie](#session-cookie) |
| Contradictory `__Host-` / `__Secure-` cookie settings are rejected | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | `UserWarning` (`FutureWarning` when `cookie_name` is left at its default, under the middleware only) | [Session cookie](#session-cookie) |
| Explicit `login()` / `logout()`, read-only `Session.user` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | No | [Login and logout](#login-logout) |
| `get_or_set()` locks by default | [#280](https://github.com/allen0099/FastAPI-CacheX/issues/280) | `FutureWarning` | [get_or_set lock](#get-or-set-lock) |
| `get_session_manager` resolves through `SessionManagerProxy` | [#131](https://github.com/allen0099/FastAPI-CacheX/issues/131) | `FutureWarning` | [get_session_manager](#get-session-manager) |
Expand Down Expand Up @@ -83,7 +83,7 @@ config = SessionConfig(
)
```

0.4.0 also rejects contradictory settings: a `__Host-` name without `cookie_https_only=True`, with a `cookie_path` other than `"/"` or with a `cookie_domain`, and a `__Secure-` name without `cookie_https_only=True`. Browsers already refuse such a cookie, so the session never sticks; 0.3.9 emits a `UserWarning` when `SessionConfig` is built with one of them.
0.4.0 also rejects contradictory settings: a `__Host-` name without `cookie_https_only=True`, with a `cookie_path` other than `"/"` or with a `cookie_domain`, and a `__Secure-` name without `cookie_https_only=True`. Browsers already refuse such a cookie, so the session never sticks; 0.3.9 emits a `UserWarning` when `SessionConfig` is built with one of them. The rejection is a `pydantic.ValidationError` (a `ValueError`) from `SessionConfig`. Because the default name is now `__Host-session`, a config that sets only `cookie_https_only=False`, a `cookie_path` or a `cookie_domain` and leaves `cookie_name` at its default raises too. 0.3.9 warned about that only under `FastAPICacheXSessionMiddleware`, with the `FutureWarning` above; a header-only setup that sets one of these options got no warning. Set `cookie_name` as well, as in the first After example, or remove the option if nothing reads the cookie.

### Login and logout {#login-logout}

Expand Down
23 changes: 10 additions & 13 deletions docs/SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The header-only `SessionMiddleware`, deprecated since 0.3.1, was **removed in 0.
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 `SessionManager` is used
without it.
without it, although `SessionConfig` still validates them (see [Cookie defaults](#cookie-defaults)).

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 @@ -249,28 +249,25 @@ SessionConfig(
# Backend
backend_key_prefix="session:",
# Cookies (read only by FastAPICacheXSessionMiddleware)
cookie_name="session", # "__Host-session" from 0.4.0
cookie_name="__Host-session", # see "Cookie defaults" below
cookie_max_age=14
* 24
* 60
* 60, # None = no Max-Age (cookie ends with the browser session)
cookie_path="/",
cookie_same_site="lax", # "lax" / "strict" / "none" ("none" needs cookie_https_only=True)
cookie_https_only=False, # True adds the Secure flag; True from 0.4.0
cookie_https_only=True, # the Secure flag: the cookie is only sent over HTTPS
cookie_domain=None, # None = no Domain attribute
)
```

#### Cookie defaults change in 0.4.0 {#cookie-defaults-change-in-040}
#### Cookie defaults {#cookie-defaults}

0.4.0 names the session cookie `__Host-session` and sets the `Secure` flag by default. Browsers accept a `__Host-` cookie only when it is `Secure`, has `Path=/` and no `Domain`, and never from a subdomain, which removes the usual way to plant a session cookie (session fixation). The new name also means every browser holding a `session` cookie is logged out once after the upgrade.
The session cookie is named `__Host-session` and carries the `Secure` flag by default. Browsers accept a `__Host-` cookie only when it is `Secure`, has `Path=/` and no `Domain`, and never from a subdomain, which removes the usual way to plant a session cookie (session fixation).

Until then, `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when it is constructed (when the app builds its middleware stack, at startup or on the first request) and its config leaves `cookie_name` or `cookie_https_only` at the default. Set both to silence it:
A `Secure` cookie is not sent over plain HTTP. For local development without TLS, name the cookie without the prefix and drop the flag: `cookie_name="session", cookie_https_only=False`.

- `cookie_name="session", cookie_https_only=False` keeps the current cookie (and keeps working after the upgrade, e.g. for local development over plain HTTP);
- `cookie_name="__Host-session", cookie_https_only=True` switches now, over HTTPS.

A `__Host-` name with `cookie_https_only=False`, a `cookie_path` other than `/` or a `cookie_domain` (and a `__Secure-` name without `cookie_https_only=True`) emits a `UserWarning`, because browsers refuse such a cookie; 0.4.0 rejects the combination. See [Migrating to 0.4.0](MIGRATING_0_4.md#session-cookie).
`SessionConfig` raises a `ValidationError` for a cookie browsers would refuse: a `__Host-` name with `cookie_https_only=False`, a `cookie_path` other than `/` or a `cookie_domain`, and a `__Secure-` name without `cookie_https_only=True`. The default name has the `__Host-` prefix, so changing only one of those settings raises too; change `cookie_name` with it. Upgrading from 0.3.x changes the cookie name, which logs every cookie session out once; see [Migrating to 0.4.0](MIGRATING_0_4.md#session-cookie).

Sessions expire after `session_ttl` seconds. With `sliding_expiration`, each request that finds
less than `session_ttl * sliding_threshold` seconds remaining extends the expiry to a full
Expand Down Expand Up @@ -376,13 +373,13 @@ issued so far.

### 2. HTTPS Only

Always transport tokens over HTTPS in production. For cookie clients, mark the cookie `Secure`:
Always transport tokens over HTTPS in production. For cookie clients, keep the cookie `Secure`, as it is by default:

```python
config = SessionConfig(
secret_key="...",
cookie_name="__Host-session", # browsers refuse it without Secure, Path=/, no Domain
cookie_https_only=True, # adds the Secure flag to the session cookie
cookie_name="__Host-session", # the default; browsers refuse it without Secure, Path=/, no Domain
cookie_https_only=True, # the default; adds the Secure flag to the session cookie
)
```

Expand Down
5 changes: 3 additions & 2 deletions examples/session_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ def session_secret_key() -> str:
# At least 32 characters, from the environment; see session_secret_key().
secret_key=session_secret_key(),
session_ttl=3600, # 1 hour
# Both cookie defaults change in 0.4.0, so set them explicitly. Over HTTPS
# use cookie_name="__Host-session", cookie_https_only=True.
# The default cookie (__Host-session with the Secure flag) needs HTTPS.
# This example runs over plain HTTP, so it names the cookie without the
# __Host- prefix and drops Secure; in production, leave both out.
cookie_name="session",
cookie_https_only=False,
)
Expand Down
6 changes: 3 additions & 3 deletions examples/session_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def session_secret_key() -> str:
# Optional: issued as `iss`/`aud` and checked on every request.
jwt_issuer="https://api.example.com",
jwt_audience="example-clients",
# The middleware also accepts a cookie. 0.4.0 changes both cookie defaults
# (to "__Host-session" with the Secure flag), so set them explicitly; over
# HTTPS use cookie_name="__Host-session" and cookie_https_only=True.
# The middleware also accepts a cookie. The default one (__Host-session
# with the Secure flag) needs HTTPS; this example runs over plain HTTP, so
# it names the cookie without the __Host- prefix and drops Secure.
cookie_name="session",
cookie_https_only=False,
)
Expand Down
3 changes: 1 addition & 2 deletions examples/session_jwt_claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@ def session_secret_key() -> str:
jwt_algorithm="HS256",
jwt_issuer="acme-corp",
jwt_audience="acme-api",
cookie_name="__Host-session", # the middleware also reads a cookie
cookie_https_only=True,
# The middleware also reads a cookie: __Host-session with Secure by default.
)

# token_serializer replaces the serializer chosen from token_format.
Expand Down
7 changes: 3 additions & 4 deletions examples/session_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,9 @@ def session_secret_key() -> str:
# At least 32 characters, from the environment; see session_secret_key().
secret_key=session_secret_key(),
session_ttl=3600,
# 0.4.0 changes both cookie defaults (to "__Host-session" with the Secure
# flag), so set them explicitly. In production over HTTPS use
# cookie_name="__Host-session" and cookie_https_only=True; keep False only
# for local HTTP development.
# The default cookie (__Host-session with the Secure flag) needs HTTPS.
# This example runs over plain HTTP, so it names the cookie without the
# __Host- prefix and drops Secure; in production, leave both out.
cookie_name="session",
cookie_https_only=False,
)
Expand Down
3 changes: 1 addition & 2 deletions examples/session_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ def session_secret_key() -> str:
sliding_threshold=0.5,
ip_binding=True, # reject the token from another IP address
user_agent_binding=False, # optional: reject it from another User-Agent
cookie_name="__Host-session", # the 0.4.0 default; needs HTTPS
cookie_https_only=True,
# The cookie defaults to __Host-session with the Secure flag: HTTPS only.
)
session_manager = SessionManager(backend, config)
# From 0.4.0 ClientIPDep resolves the manager only through the proxy.
Expand Down
49 changes: 33 additions & 16 deletions fastapi_cachex/session/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class SessionConfig(BaseModel):

# Cookie settings (FastAPICacheXSessionMiddleware only)
cookie_name: str = Field(
default="session",
default="__Host-session",
description="Name of the cookie used to store the session token "
"(FastAPICacheXSessionMiddleware only)",
)
Expand All @@ -188,7 +188,7 @@ class SessionConfig(BaseModel):
description="SameSite attribute for the session cookie",
)
cookie_https_only: bool = Field(
default=False,
default=True,
description="Whether to set the Secure flag on the session cookie "
"(cookie only sent over HTTPS)",
)
Expand Down Expand Up @@ -238,9 +238,15 @@ def _warn_insecure_same_site_none(self) -> "SessionConfig":
"""Warn about a SameSite=None cookie without the Secure flag.

Browsers drop such a cookie, so the session would silently never stick.
Rejecting the combination would break existing configurations.
Rejecting the combination would break existing configurations. A
``__Host-`` / ``__Secure-`` name without Secure is rejected by
``_check_cookie_prefix`` instead, so it is not warned about twice.
"""
if self.cookie_same_site == "none" and not self.cookie_https_only:
if (
self.cookie_same_site == "none"
and not self.cookie_https_only
and not self.cookie_name.startswith(("__Host-", "__Secure-"))
):
warnings.warn(
'cookie_same_site="none" requires cookie_https_only=True: browsers '
"reject a SameSite=None cookie without the Secure flag, so the "
Expand All @@ -251,13 +257,12 @@ def _warn_insecure_same_site_none(self) -> "SessionConfig":
return self

@model_validator(mode="after")
def _warn_invalid_cookie_prefix(self) -> "SessionConfig":
"""Warn about a ``__Host-`` / ``__Secure-`` cookie browsers will refuse.
def _check_cookie_prefix(self) -> "SessionConfig":
"""Reject a ``__Host-`` / ``__Secure-`` cookie browsers would refuse.

Browsers store a ``__Secure-`` cookie only with the Secure flag, and a
``__Host-`` cookie only with Secure, ``Path=/`` and no ``Domain``, so
the session would silently never stick. 0.4.0 rejects these
combinations (#256).
the session would silently never stick (#256).
"""
problems: list[str] = []
if self.cookie_name.startswith(("__Host-", "__Secure-")):
Expand All @@ -269,15 +274,27 @@ def _warn_invalid_cookie_prefix(self) -> "SessionConfig":
if self.cookie_domain is not None:
problems.append("cookie_domain=None")
if problems:
warnings.warn(
f"cookie_name={self.cookie_name!r} requires {', '.join(problems)}: "
"browsers refuse a cookie with this prefix otherwise, so the "
"session cookie would never be stored. Version 0.4.0 will reject "
"this configuration "
"(https://github.com/allen0099/FastAPI-CacheX/issues/256).",
UserWarning,
stacklevel=3,
name = f"cookie_name={self.cookie_name!r}"
if "cookie_name" not in self.model_fields_set:
name += " (the default)"
hints: list[str] = []
if "cookie_https_only=True" in problems:
hints.append(
"For plain-HTTP development, use a name without the prefix, "
"such as cookie_name='session' with cookie_https_only=False."
)
if self.cookie_path != "/" or self.cookie_domain is not None:
hints.append(
"To set a cookie_path or cookie_domain, use "
"cookie_name='__Secure-session', which keeps the Secure flag."
)
msg = (
f"{name} requires {', '.join(problems)}: browsers refuse a cookie "
"with this prefix otherwise, so the session cookie would never be "
f"stored. {' '.join(hints)} See "
"https://fastapi-cachex.readthedocs.io/en/stable/SESSION/#cookie-defaults"
)
raise ValueError(msg)
return self

@field_validator("token_source_priority")
Expand Down
39 changes: 2 additions & 37 deletions fastapi_cachex/session/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,38 +163,6 @@ def _stash_session_manager(app: Any, manager: SessionManager) -> None:
setattr(app.state, "__fastapi_cachex_session_manager", manager)


def _warn_if_default_cookie(config: SessionConfig) -> None:
"""Warn that 0.4.0 changes the session cookie defaults this config relies on.

0.4.0 names the cookie ``__Host-session`` and sets the Secure flag by
default (#256). The new name logs every cookie session out once, and the
Secure flag stops the cookie working over plain HTTP, so both have to be
chosen explicitly to be unaffected.

Args:
config: The configuration the middleware uses
"""
defaults = [
name
for name in ("cookie_name", "cookie_https_only")
if name not in config.model_fields_set
]
if not defaults:
return
warnings.warn(
f"FastAPICacheXSessionMiddleware is using the default {' and '.join(defaults)} "
"of SessionConfig. Version 0.4.0 changes the session cookie defaults to "
"cookie_name='__Host-session' with the Secure flag (cookie_https_only=True): "
"the new name logs every cookie session out once on upgrade, and a Secure "
"cookie is not sent over plain HTTP. Set both explicitly: "
"cookie_name='session', cookie_https_only=False keeps the current cookie; "
"cookie_name='__Host-session', cookie_https_only=True switches now "
"(https://github.com/allen0099/FastAPI-CacheX/issues/256).",
FutureWarning,
stacklevel=3,
)


def _warn_if_priority_without_cookie(config: SessionConfig) -> None:
"""Warn that 0.4.0 stops using the cookie for a list without ``"cookie"``.

Expand Down Expand Up @@ -279,15 +247,12 @@ def __init__(
``SessionManagerProxy`` holds none.

Warns:
FutureWarning: If the configuration leaves ``cookie_name`` or
``cookie_https_only`` at its default, both of which change in
0.4.0, or sets ``token_source_priority`` without ``"cookie"``,
which disables the cookie in 0.4.0.
FutureWarning: If the configuration sets ``token_source_priority``
without ``"cookie"``, which disables the cookie in 0.4.0.
"""
self.app = app
self.session_manager = session_manager or SessionManagerProxy.get()
self.config = config or self.session_manager.config
_warn_if_default_cookie(self.config)
_warn_if_priority_without_cookie(self.config)

security_flags = f"httponly; samesite={self.config.cookie_same_site}"
Expand Down
Loading
Loading