From e540d3e2f473ebf07ea7ed6f4aa278ab856ae6ab Mon Sep 17 00:00:00 2001 From: allen0099 Date: Tue, 29 Sep 2026 12:10:50 +0000 Subject: [PATCH] feat(session): warn about the token_source_priority and use_bearer_token changes in 0.3.9 0.4.0 makes token_source_priority name every token source, with the default ["header", "bearer", "cookie"], and a list without "cookie" disables the session cookie (#75). The list now accepts "cookie" as its last entry, where the cookie is read today, and FastAPICacheXSessionMiddleware emits a FutureWarning when the list was set explicitly without it. use_bearer_token duplicates the list and is removed in 0.4.0 (#377); passing it now emits a DeprecationWarning. --- changelog.d/377.deprecated.md | 6 ++ changelog.d/75.deprecated.md | 9 ++ docs/MIGRATING_0_4.md | 27 +++++- docs/SESSION.md | 41 ++++++--- fastapi_cachex/session/config.py | 50 +++++++++- fastapi_cachex/session/middleware.py | 42 ++++++++- i18n/zh-TW/docs/MIGRATING_0_4.md | 27 +++++- i18n/zh-TW/docs/SESSION.md | 14 ++- tests/session/test_0_4_0_notices.py | 102 +++++++++++++++++++++ tests/session/test_middleware.py | 7 +- tests/session/test_starlette_middleware.py | 13 +-- 11 files changed, 301 insertions(+), 37 deletions(-) create mode 100644 changelog.d/377.deprecated.md create mode 100644 changelog.d/75.deprecated.md diff --git a/changelog.d/377.deprecated.md b/changelog.d/377.deprecated.md new file mode 100644 index 0000000..bf0d7d4 --- /dev/null +++ b/changelog.d/377.deprecated.md @@ -0,0 +1,6 @@ +**`SessionConfig.use_bearer_token`.** `token_source_priority` already decides +whether bearer tokens are read, and 0.4.0 removes the flag. Passing it now +emits a `DeprecationWarning`, whatever its value. Replace +`use_bearer_token=False` by leaving `"bearer"` out of the list (for example +`token_source_priority=["header", "cookie"]`); `use_bearer_token=True` is the +default and can be dropped. diff --git a/changelog.d/75.deprecated.md b/changelog.d/75.deprecated.md new file mode 100644 index 0000000..2092209 --- /dev/null +++ b/changelog.d/75.deprecated.md @@ -0,0 +1,9 @@ +**Setting `token_source_priority` without `"cookie"`.** In 0.4.0 the list names +every token source. Its default becomes `["header", "bearer", "cookie"]`, the +order used today, and a list without `"cookie"` means the middleware neither +reads nor sets the session cookie. The list now accepts `"cookie"` as its last +entry, which changes nothing yet since the cookie is read there anyway; any +other position raises a `ValidationError`. `FastAPICacheXSessionMiddleware` +now emits a `FutureWarning` when the list was set explicitly without +`"cookie"`: add it as the last entry to keep the cookie. The default list does +not warn. See "Migrating to 0.4.0" in the docs. diff --git a/docs/MIGRATING_0_4.md b/docs/MIGRATING_0_4.md index d412568..2b83999 100644 --- a/docs/MIGRATING_0_4.md +++ b/docs/MIGRATING_0_4.md @@ -31,6 +31,7 @@ Every warning below names the setting to change and links to its issue. `FutureW | 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) | +| `token_source_priority` names every token source; a list without `"cookie"` disables the cookie | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | `FutureWarning` | [Token sources](#token-source-priority) | | `add_routes()` requires `dependencies`, no content preview by default | [#298](https://github.com/allen0099/FastAPI-CacheX/issues/298) | `UserWarning` | [Monitoring routes](#add-routes) | | Redis `encoding` option removed | [#126](https://github.com/allen0099/FastAPI-CacheX/issues/126) | `DeprecationWarning` (`RuntimeWarning` for a value other than UTF-8) | [Redis encoding](#redis-encoding) | | JWT HMAC secrets shorter than the hash output are rejected | [#129](https://github.com/allen0099/FastAPI-CacheX/issues/129) | `UserWarning` | [JWT secret length](#jwt-secret) | @@ -38,13 +39,13 @@ Every warning below names the setting to change and links to its issue. `FutureW | `BackendProxy.get_backend()` / `set_backend()` removed | [#70](https://github.com/allen0099/FastAPI-CacheX/issues/70) | `DeprecationWarning` | [BackendProxy](#backend-proxy) | | `CacheError` removed | [#130](https://github.com/allen0099/FastAPI-CacheX/issues/130) | `DeprecationWarning` | [CacheError](#cache-error) | | Redis `clear_pattern()` prefix-stripped retry removed | [#125](https://github.com/allen0099/FastAPI-CacheX/issues/125) | `DeprecationWarning` | [Redis clear_pattern](#redis-clear-pattern) | +| `SessionConfig.use_bearer_token` removed | [#377](https://github.com/allen0099/FastAPI-CacheX/issues/377) | `DeprecationWarning` | [Token sources](#token-source-priority) | | `UserSessionDep` requires a user | [#127](https://github.com/allen0099/FastAPI-CacheX/issues/127) | No | [UserSessionDep](#user-session-dep) | | `memcache` extra removed | [#202](https://github.com/allen0099/FastAPI-CacheX/issues/202) | No | [memcache extra](#memcache-extra) | | `BaseCacheBackend.delete()` returns `bool` | [#71](https://github.com/allen0099/FastAPI-CacheX/issues/71) | No | [delete() return value](#backend-delete) | | `CacheEntry.headers` becomes a list of pairs | [#105](https://github.com/allen0099/FastAPI-CacheX/issues/105) | No | [Repeated headers](#cache-entry-headers) | | HTTP cache key format | [#271](https://github.com/allen0099/FastAPI-CacheX/issues/271), [#270](https://github.com/allen0099/FastAPI-CacheX/issues/270), [#269](https://github.com/allen0099/FastAPI-CacheX/issues/269), [#266](https://github.com/allen0099/FastAPI-CacheX/issues/266), [#265](https://github.com/allen0099/FastAPI-CacheX/issues/265), [#72](https://github.com/allen0099/FastAPI-CacheX/issues/72) | No | [Cache keys](#cache-keys) | | Conditional session writes | [#128](https://github.com/allen0099/FastAPI-CacheX/issues/128) | No | [Session writes](#session-writes) | -| Cookies in `token_source_priority` | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | No | [Token sources](#token-source-priority) | ## Sessions {#sessions} @@ -186,7 +187,29 @@ SessionConfig( ### Token sources {#token-source-priority} -`SessionConfig.token_source_priority` accepts `"cookie"` next to `"header"` and `"bearer"` in 0.4.0 ([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)), and `FastAPICacheXSessionMiddleware` resolves the token by walking the list. Existing lists keep working; today the cookie is read after the header and bearer sources. Whether the default list gains `"cookie"`, and in which position, is not decided yet. +In 0.4.0, `SessionConfig.token_source_priority` names every token source ([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)). `FastAPICacheXSessionMiddleware` resolves the token by walking the list, `"cookie"` may appear anywhere in it, and the default becomes `["header", "bearer", "cookie"]`, which is the order used today. A list without `"cookie"` means no cookie: the middleware neither reads nor sets it, and a session created for a request without a token sends its token in the `header_name` response header. That is how an API-only app turns the cookie off. + +In 0.3.9, `"cookie"` is accepted as the last entry (where the cookie is read today, so it changes nothing yet), and `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when the list was set explicitly without it. The default list does not warn. + +```python +# Before: the cookie is read after these, although the list does not say so +SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header"]) + +# After, keeping the cookie (works on 0.3.9 and 0.4.0) +SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header", "cookie"]) +``` + +`use_bearer_token` is removed ([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)); the list alone decides whether bearer tokens are read. 0.3.9 emits a `DeprecationWarning` when it is passed, whatever its value: + +```python +# Before +SessionConfig(secret_key=SECRET, use_bearer_token=False) + +# After +SessionConfig(secret_key=SECRET, token_source_priority=["header", "cookie"]) +``` + +`use_bearer_token=True` is the default and can be dropped. ## Application cache {#application-cache} diff --git a/docs/SESSION.md b/docs/SESSION.md index 71c33ea..782cdeb 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -235,8 +235,11 @@ SessionConfig( # Token sources (API-first architecture) token_format="simple", # "simple" (default) or "jwt" header_name="X-Session-Token", - use_bearer_token=True, - token_source_priority=["header", "bearer"], # only these two values (see below) + use_bearer_token=True, # deprecated: leave "bearer" out of token_source_priority instead + token_source_priority=[ + "header", + "bearer", + ], # "cookie" only as the last entry (see below) # JWT (used when token_format == "jwt") jwt_algorithm="HS256", # "none" is rejected jwt_issuer=None, # if set, iss is written and verified on parsing @@ -282,17 +285,29 @@ session that many seconds after it was created, regardless of sliding renewals: the backend TTL and a JWT's `exp` never go past `created_at + absolute_timeout`, and once the expiry reaches that cap no further renewed tokens are issued. -#### `token_source_priority` accepts only `"header"` and `"bearer"` - -The field's type is `list[Literal["header", "bearer"]]`; passing `"cookie"` is rejected by -Pydantic with a `ValidationError`. The cookie is **not** part of the priority order: -`FastAPICacheXSessionMiddleware` resolves tokens in a fixed order — it first reads -header/bearer following `token_source_priority`, and only falls back to the cookie when neither -yields a token. This is deliberate: the response side is routed by the token's source (header in, -header out; cookie in, `Set-Cookie` out), and mixing the cookie into the same priority list would -make a `["cookie"]` setting fail silently on the deprecated `SessionMiddleware`. Once -`SessionMiddleware` is removed in 0.4.0, all three sources may be described by a single priority -list. +#### `token_source_priority` and the session cookie + +`FastAPICacheXSessionMiddleware` first reads the header sources in `token_source_priority` order, +and only falls back to the cookie when none of them yields a token. The response side follows the +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. + +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 +middleware neither reads nor sets it, and a session created for a request without a token sends +its token in the `header_name` response header ([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)). +So `FastAPICacheXSessionMiddleware` emits a `FutureWarning` when the list was set explicitly +without `"cookie"`. Add `"cookie"` as the last entry to keep the cookie; the default list does not +warn. See [Migrating to 0.4.0](MIGRATING_0_4.md#token-source-priority). + +`use_bearer_token` is deprecated and removed in 0.4.0 ([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)): +passing it emits a `DeprecationWarning`. Instead of `use_bearer_token=False`, leave `"bearer"` out +of the list (`token_source_priority=["header", "cookie"]`); `use_bearer_token=True` is the +default and can simply be dropped. **Header/bearer clients** should store the token in `localStorage` or `sessionStorage` and send it as `Authorization: Bearer ` or `X-Session-Token: `. **Cookie clients** (browsers) diff --git a/fastapi_cachex/session/config.py b/fastapi_cachex/session/config.py index a46c0c6..6cb5622 100644 --- a/fastapi_cachex/session/config.py +++ b/fastapi_cachex/session/config.py @@ -107,11 +107,14 @@ class SessionConfig(BaseModel): ) use_bearer_token: bool = Field( default=True, - description="Whether to accept Authorization Bearer tokens", + description="Whether to accept Authorization Bearer tokens (deprecated: " + 'leave "bearer" out of token_source_priority instead; removed in 0.4.0)', ) - token_source_priority: list[Literal["header", "bearer"]] = Field( + token_source_priority: list[Literal["header", "bearer", "cookie"]] = Field( default=["header", "bearer"], - description="Priority order for token sources", + description="Priority order for token sources. The session cookie is " + 'read after the header sources; "cookie" may only be the last entry ' + "until 0.4.0, which drops the cookie from a list without it", ) # JWT settings (used when token_format == 'jwt') @@ -276,6 +279,47 @@ def _warn_invalid_cookie_prefix(self) -> "SessionConfig": ) return self + @field_validator("token_source_priority") + @classmethod + def _check_cookie_is_last( + cls, value: list[Literal["header", "bearer", "cookie"]] + ) -> list[Literal["header", "bearer", "cookie"]]: + """Only accept ``"cookie"`` where the cookie is read today: last. + + ``FastAPICacheXSessionMiddleware`` reads the cookie after the header + sources, so listing it last changes nothing before 0.4.0, when the + list decides the order and a list without it disables the cookie (#75). + """ + if "cookie" in value and value.index("cookie") != len(value) - 1: + msg = ( + '"cookie" must be the last entry of token_source_priority: the ' + "session cookie is read after the header sources until 0.4.0 " + "(https://github.com/allen0099/FastAPI-CacheX/issues/75)" + ) + raise ValueError(msg) + return value + + @model_validator(mode="after") + def _warn_use_bearer_token(self) -> "SessionConfig": + """Deprecate ``use_bearer_token``, which 0.4.0 removes (#377). + + ``token_source_priority`` already decides whether bearer tokens are + read, so passing the flag at all warns, whatever its value. + """ + if "use_bearer_token" in self.model_fields_set: + warnings.warn( + "SessionConfig(use_bearer_token=...) is deprecated and will be " + "removed in version 0.4.0. token_source_priority decides the " + "token sources: drop use_bearer_token=True (the default), and " + 'replace use_bearer_token=False by leaving "bearer" out of ' + 'token_source_priority, keeping "cookie" last, e.g. ' + '["header", "cookie"] ' + "(https://github.com/allen0099/FastAPI-CacheX/issues/377).", + DeprecationWarning, + stacklevel=3, + ) + return self + @field_validator("jwt_algorithm") @classmethod def _check_jwt_algorithm(cls, value: str) -> str: diff --git a/fastapi_cachex/session/middleware.py b/fastapi_cachex/session/middleware.py index 8431727..3c32131 100644 --- a/fastapi_cachex/session/middleware.py +++ b/fastapi_cachex/session/middleware.py @@ -123,8 +123,9 @@ def _read_header_token( """ consulted: list[str] = [] # `token_source_priority` is a list of Literals, so pydantic has already - # rejected anything that is neither branch; the chain stays an `elif` so a - # source added later falls through instead of being read as a bearer token. + # rejected anything else; the chain stays an `elif` so "cookie" (read by + # FastAPICacheXSessionMiddleware after these) and any source added later + # fall through instead of being read as a bearer token. for source in config.token_source_priority: if source == "header": consulted.append(config.header_name) @@ -218,13 +219,44 @@ def _warn_if_default_cookie(config: SessionConfig) -> None: ) +def _warn_if_priority_without_cookie(config: SessionConfig) -> None: + """Warn that 0.4.0 stops using the cookie for a list without ``"cookie"``. + + In 0.4.0 ``token_source_priority`` lists every token source, so a list + set without ``"cookie"`` means the middleware neither reads nor sets the + session cookie (#75). The default list becomes ``["header", "bearer", + "cookie"]``, which is today's order, so it does not warn. + + Args: + config: The configuration the middleware uses + """ + if ( + "token_source_priority" not in config.model_fields_set + or "cookie" in config.token_source_priority + ): + return + warnings.warn( + f"SessionConfig(token_source_priority={config.token_source_priority!r}) " + 'does not list "cookie". In version 0.4.0 the list names every token ' + "source, so FastAPICacheXSessionMiddleware will neither read nor set the " + "session cookie, and a session created for a request without a token " + 'sends its token in the header_name response header. Add "cookie" as the ' + "last entry to keep the cookie; leaving it out opts into 0.4.0's " + "cookie-less behaviour, which only takes effect then " + "(https://github.com/allen0099/FastAPI-CacheX/issues/75).", + FutureWarning, + stacklevel=3, + ) + + 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 currently supported. + 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 @@ -413,12 +445,14 @@ def __init__( Warns: FutureWarning: If the configuration leaves ``cookie_name`` or ``cookie_https_only`` at its default, both of which change in - 0.4.0. + 0.4.0, or 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}" if self.config.cookie_https_only: diff --git a/i18n/zh-TW/docs/MIGRATING_0_4.md b/i18n/zh-TW/docs/MIGRATING_0_4.md index abc5476..cbfe73f 100644 --- a/i18n/zh-TW/docs/MIGRATING_0_4.md +++ b/i18n/zh-TW/docs/MIGRATING_0_4.md @@ -31,6 +31,7 @@ filterwarnings = [ | 明確的 `login()`/`logout()`、唯讀的 `Session.user` | [#256](https://github.com/allen0099/FastAPI-CacheX/issues/256) | 否 | [登入與登出](#login-logout) | | `get_or_set()` 預設使用鎖 | [#280](https://github.com/allen0099/FastAPI-CacheX/issues/280) | `FutureWarning` | [get_or_set 的鎖](#get-or-set-lock) | | `get_session_manager` 透過 `SessionManagerProxy` 取得 | [#131](https://github.com/allen0099/FastAPI-CacheX/issues/131) | `FutureWarning` | [get_session_manager](#get-session-manager) | +| `token_source_priority` 列出所有權杖來源;沒有 `"cookie"` 的清單會停用 Cookie | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | `FutureWarning` | [權杖來源](#token-source-priority) | | `add_routes()` 必須傳入 `dependencies`,預設不含內容預覽 | [#298](https://github.com/allen0099/FastAPI-CacheX/issues/298) | `UserWarning` | [監控路由](#add-routes) | | 移除 Redis 的 `encoding` 選項 | [#126](https://github.com/allen0099/FastAPI-CacheX/issues/126) | `DeprecationWarning`(UTF-8 以外的值為 `RuntimeWarning`) | [Redis encoding](#redis-encoding) | | 拒絕短於雜湊輸出的 JWT HMAC 密鑰 | [#129](https://github.com/allen0099/FastAPI-CacheX/issues/129) | `UserWarning` | [JWT 密鑰長度](#jwt-secret) | @@ -38,13 +39,13 @@ filterwarnings = [ | 移除 `BackendProxy.get_backend()`/`set_backend()` | [#70](https://github.com/allen0099/FastAPI-CacheX/issues/70) | `DeprecationWarning` | [BackendProxy](#backend-proxy) | | 移除 `CacheError` | [#130](https://github.com/allen0099/FastAPI-CacheX/issues/130) | `DeprecationWarning` | [CacheError](#cache-error) | | 移除 Redis `clear_pattern()` 去除前綴後的重試 | [#125](https://github.com/allen0099/FastAPI-CacheX/issues/125) | `DeprecationWarning` | [Redis clear_pattern](#redis-clear-pattern) | +| 移除 `SessionConfig.use_bearer_token` | [#377](https://github.com/allen0099/FastAPI-CacheX/issues/377) | `DeprecationWarning` | [權杖來源](#token-source-priority) | | `UserSessionDep` 需要使用者 | [#127](https://github.com/allen0099/FastAPI-CacheX/issues/127) | 否 | [UserSessionDep](#user-session-dep) | | 移除 `memcache` extra | [#202](https://github.com/allen0099/FastAPI-CacheX/issues/202) | 否 | [memcache extra](#memcache-extra) | | `BaseCacheBackend.delete()` 回傳 `bool` | [#71](https://github.com/allen0099/FastAPI-CacheX/issues/71) | 否 | [delete() 的回傳值](#backend-delete) | | `CacheEntry.headers` 改為成對值的清單 | [#105](https://github.com/allen0099/FastAPI-CacheX/issues/105) | 否 | [重複的標頭](#cache-entry-headers) | | HTTP 快取鍵格式 | [#271](https://github.com/allen0099/FastAPI-CacheX/issues/271)、[#270](https://github.com/allen0099/FastAPI-CacheX/issues/270)、[#269](https://github.com/allen0099/FastAPI-CacheX/issues/269)、[#266](https://github.com/allen0099/FastAPI-CacheX/issues/266)、[#265](https://github.com/allen0099/FastAPI-CacheX/issues/265)、[#72](https://github.com/allen0099/FastAPI-CacheX/issues/72) | 否 | [快取鍵](#cache-keys) | | 有條件的 Session 寫入 | [#128](https://github.com/allen0099/FastAPI-CacheX/issues/128) | 否 | [Session 寫入](#session-writes) | -| `token_source_priority` 可包含 Cookie | [#75](https://github.com/allen0099/FastAPI-CacheX/issues/75) | 否 | [權杖來源](#token-source-priority) | ## Session {#sessions} @@ -185,7 +186,29 @@ SessionConfig( ### 權杖來源 {#token-source-priority} -0.4.0 起,`SessionConfig.token_source_priority` 除了 `"header"` 與 `"bearer"` 之外也接受 `"cookie"`([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75)),`FastAPICacheXSessionMiddleware` 會依照清單順序尋找權杖。現有的清單仍可運作;目前 Cookie 會在標頭與 bearer 來源之後讀取。預設清單是否加入 `"cookie"`、放在什麼位置,尚未決定。 +0.4.0 起,`SessionConfig.token_source_priority` 列出所有權杖來源([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75))。`FastAPICacheXSessionMiddleware` 會依照清單順序尋找權杖,`"cookie"` 可以放在任何位置,預設值改為 `["header", "bearer", "cookie"]`,也就是目前使用的順序。沒有 `"cookie"` 的清單代表不使用 Cookie:中介軟體既不讀取也不設定它,而為沒有權杖的請求建立的 Session 會在 `header_name` 回應標頭中送出權杖。純 API 的應用就是用這種方式關閉 Cookie。 + +0.3.9 接受把 `"cookie"` 放在最後一項(也就是目前讀取 Cookie 的位置,因此暫時不會改變任何行為);明確設定的清單沒有它時,`FastAPICacheXSessionMiddleware` 會發出 `FutureWarning`。預設清單不會發出警告。 + +```python +# 之前:雖然清單沒寫,Cookie 仍會在這兩者之後被讀取 +SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header"]) + +# 之後,保留 Cookie(在 0.3.9 與 0.4.0 皆可運作) +SessionConfig(secret_key=SECRET, token_source_priority=["bearer", "header", "cookie"]) +``` + +`use_bearer_token` 已移除([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)),是否讀取 Bearer 權杖只由清單決定。0.3.9 在傳入它時,不論值為何,都會發出 `DeprecationWarning`: + +```python +# 之前 +SessionConfig(secret_key=SECRET, use_bearer_token=False) + +# 之後 +SessionConfig(secret_key=SECRET, token_source_priority=["header", "cookie"]) +``` + +`use_bearer_token=True` 是預設值,直接拿掉即可。 ## 應用層快取 {#application-cache} diff --git a/i18n/zh-TW/docs/SESSION.md b/i18n/zh-TW/docs/SESSION.md index ab10b5d..de8d9bf 100644 --- a/i18n/zh-TW/docs/SESSION.md +++ b/i18n/zh-TW/docs/SESSION.md @@ -142,8 +142,8 @@ SessionConfig( # 權杖來源(API 優先架構) token_format="simple", # "simple"(預設)或 "jwt" header_name="X-Session-Token", - use_bearer_token=True, - token_source_priority=["header", "bearer"], # 只接受這兩個值(見下文) + use_bearer_token=True, # 已棄用:改為不在 token_source_priority 中列出 "bearer" + token_source_priority=["header", "bearer"], # "cookie" 只能放在最後(見下文) # JWT(token_format == "jwt" 時使用) jwt_algorithm="HS256", # 拒絕 "none" jwt_issuer=None, # 若有設定,會寫入 iss 並在解析時驗證 @@ -182,9 +182,15 @@ SessionConfig( Session 會在 `session_ttl` 秒後過期。啟用 `sliding_expiration` 時,每個發現剩餘時間少於 `session_ttl * sliding_threshold` 秒的請求,都會將過期時間重新延長為完整的 `session_ttl`,並發行一個更新後的權杖,由中介軟體傳回給用戶端(回應標頭或 `Set-Cookie`,見上表)。標頭/Bearer 用戶端在回應帶有 `header_name` 標頭時,應以它取代已保存的權杖。`absolute_timeout` 會在 Session 建立後經過該秒數時結束 Session,不論是否有滑動更新:過期時間、後端 TTL 與 JWT 的 `exp` 都不會超過 `created_at + absolute_timeout`,過期時間到達這個上限後也不再發行更新後的權杖。 -#### `token_source_priority` 只接受 `"header"` 與 `"bearer"` {#token_source_priority-accepts-only-header-and-bearer} +#### `token_source_priority` 與 Session Cookie {#token_source_priority-and-the-session-cookie} -此欄位的型別為 `list[Literal["header", "bearer"]]`;傳入 `"cookie"` 會被 Pydantic 以 `ValidationError` 拒絕。Cookie **不**屬於優先順序的一部分:`FastAPICacheXSessionMiddleware` 以固定順序解析權杖——它先依照 `token_source_priority` 讀取標頭/Bearer,只有兩者都沒有產生權杖時才退回使用 Cookie。這是刻意的設計:回應端是依權杖的來源決定(標頭進、標頭出;Cookie 進、`Set-Cookie` 出),若將 Cookie 混入同一個優先順序清單,`["cookie"]` 這個設定在已棄用的 `SessionMiddleware` 上就會無聲地失效。等到 `SessionMiddleware` 於 0.4.0 移除後,三種來源或許可以用單一的優先順序清單描述。 +`FastAPICacheXSessionMiddleware` 先依照 `token_source_priority` 的順序讀取標頭來源,只有它們都沒有產生權杖時才退回使用 Cookie。回應端依權杖的來源決定(標頭進、標頭出;Cookie 進、`Set-Cookie` 出)。 + +在 0.4.0 以前,不論清單是否列出 Cookie,都會讀取 Cookie。`"cookie"` 只能放在清單的最後一項,也就是它現在本來就被讀取的位置,因此列出它目前不會改變任何行為;放在其他位置會引發 `ValidationError`。已棄用的 `SessionMiddleware` 從不讀取 Cookie,會忽略這一項。 + +0.4.0 起,這個清單列出所有權杖來源,預設值改為 `["header", "bearer", "cookie"]`,也就是目前使用的順序。沒有 `"cookie"` 的清單代表完全不使用 Cookie:中介軟體既不讀取也不設定它,而為沒有權杖的請求建立的 Session 會在 `header_name` 回應標頭中送出權杖([#75](https://github.com/allen0099/FastAPI-CacheX/issues/75))。因此,明確設定了不含 `"cookie"` 的清單時,`FastAPICacheXSessionMiddleware` 會發出 `FutureWarning`。要保留 Cookie,請把 `"cookie"` 加在最後一項;預設清單不會發出警告。見[遷移至 0.4.0](MIGRATING_0_4.md#token-source-priority)。 + +`use_bearer_token` 已棄用,並於 0.4.0 移除([#377](https://github.com/allen0099/FastAPI-CacheX/issues/377)):傳入它會發出 `DeprecationWarning`。請以不在清單中列出 `"bearer"`(`token_source_priority=["header", "cookie"]`)取代 `use_bearer_token=False`;`use_bearer_token=True` 是預設值,直接拿掉即可。 **標頭/Bearer 用戶端**應將權杖存放在 `localStorage` 或 `sessionStorage`,並以 `Authorization: Bearer ` 或 `X-Session-Token: ` 送出。**Cookie 用戶端**(瀏覽器)不需要自行處理權杖,但要留意 CSRF:瀏覽器會自動附上 Cookie,因此請將 `cookie_same_site` 與你自己的 CSRF 防護搭配使用。 diff --git a/tests/session/test_0_4_0_notices.py b/tests/session/test_0_4_0_notices.py index 183db71..6e1043b 100644 --- a/tests/session/test_0_4_0_notices.py +++ b/tests/session/test_0_4_0_notices.py @@ -2,6 +2,8 @@ - #256: the default session cookie becomes ``__Host-session`` with Secure. - #131: ``get_session_manager`` resolves through ``SessionManagerProxy`` only. +- #75: a ``token_source_priority`` without ``"cookie"`` disables the cookie. +- #377: ``SessionConfig.use_bearer_token`` is removed. """ import warnings @@ -10,14 +12,17 @@ from fastapi import FastAPI from fastapi import Request from fastapi.testclient import TestClient +from pydantic import ValidationError from fastapi_cachex.backends.memory import MemoryBackend from fastapi_cachex.session import FastAPICacheXSessionMiddleware from fastapi_cachex.session import SessionConfig from fastapi_cachex.session import SessionManager from fastapi_cachex.session.dependencies import ClientIPDep +from fastapi_cachex.session.dependencies import OptionalSession from fastapi_cachex.session.dependencies import SessionManagerDep from fastapi_cachex.session.dependencies import rotate_session_id +from fastapi_cachex.session.models import SessionUser from fastapi_cachex.session.proxy import SessionManagerProxy SECRET = "a" * 32 @@ -237,3 +242,100 @@ async def read_manager(mgr: SessionManagerDep) -> dict[str, bool]: response = TestClient(app).get("/manager") assert response.json() == {"is_same": True} + + +# --- #75: token_source_priority names every token source ----------------------- + +_COOKIE = {"cookie_name": "session", "cookie_https_only": False} + + +@pytest.mark.parametrize( + "priority", [["header", "bearer"], ["bearer", "header"], ["header"], []] +) +def test_middleware_warns_for_an_explicit_list_without_cookie( + priority: list[str], +) -> None: + config = SessionConfig(secret_key=SECRET, token_source_priority=priority, **_COOKIE) + + with pytest.warns(FutureWarning, match='does not list "cookie"') as record: + _middleware(config) + + assert "issues/75" in str(record[0].message) + + +@pytest.mark.parametrize( + "settings", + [ + {}, # the default list becomes ["header", "bearer", "cookie"]: same order + {"token_source_priority": ["header", "bearer", "cookie"]}, + {"token_source_priority": ["bearer", "cookie"]}, + {"token_source_priority": ["cookie"]}, + ], +) +def test_middleware_is_silent_for_the_default_or_a_list_with_cookie( + settings: dict[str, object], +) -> None: + config = SessionConfig(secret_key=SECRET, **settings, **_COOKIE) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + _middleware(config) + + +@pytest.mark.parametrize( + "priority", [["cookie", "header"], ["header", "cookie", "bearer"]] +) +def test_cookie_is_accepted_only_as_the_last_source(priority: list[str]) -> None: + with pytest.raises(ValidationError, match="must be the last entry"): + SessionConfig(secret_key=SECRET, token_source_priority=priority) + + +async def test_the_cookie_is_still_read_before_0_4_0_with_or_without_the_entry() -> ( + None +): + """Listing "cookie" last is today's order, and leaving it out changes nothing yet.""" + backend = MemoryBackend() + for priority in (["header", "cookie"], ["header"]): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + config = SessionConfig( + secret_key=SECRET, token_source_priority=priority, **_COOKIE + ) + manager = SessionManager(backend, config) + app = FastAPI() + app.add_middleware( + FastAPICacheXSessionMiddleware, session_manager=manager, config=config + ) + + @app.get("/") + async def read(session: OptionalSession) -> dict[str, object]: + return { + "user": session.user.user_id if session and session.user else None + } + + _session, token = await manager.create_session( + user=SessionUser(user_id="u") + ) + client = TestClient(app) + client.cookies.set("session", token) + + assert client.get("/").json() == {"user": "u"}, priority + + +# --- #377: use_bearer_token is removed ------------------------------------------ + + +@pytest.mark.parametrize("value", [True, False]) +def test_passing_use_bearer_token_warns(value: bool) -> None: + with pytest.warns(DeprecationWarning, match="use_bearer_token") as record: + SessionConfig(secret_key=SECRET, use_bearer_token=value) + + message = str(record[0].message) + assert "0.4.0" in message + assert "issues/377" in message + + +def test_leaving_use_bearer_token_out_is_silent() -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + SessionConfig(secret_key=SECRET, token_source_priority=["header", "cookie"]) diff --git a/tests/session/test_middleware.py b/tests/session/test_middleware.py index 13a59f2..80ca56e 100644 --- a/tests/session/test_middleware.py +++ b/tests/session/test_middleware.py @@ -432,7 +432,8 @@ def test_header_wins_over_bearer_when_both_are_present( def test_bearer_source_is_skipped_when_bearer_tokens_are_disabled() -> None: """`use_bearer_token=False` must win over the priority list.""" - config = SessionConfig(secret_key="a" * 32, use_bearer_token=False) + with pytest.warns(DeprecationWarning, match="use_bearer_token"): + config = SessionConfig(secret_key="a" * 32, use_bearer_token=False) token = _extract_header_token( _connection({"Authorization": "Bearer from-bearer"}), config @@ -453,7 +454,7 @@ def test_an_unknown_source_is_skipped_rather_than_read_as_a_bearer_token() -> No stand where that maintainer will stand. """ config = SessionConfig(secret_key="a" * 32) - config.token_source_priority[:] = ["cookie"] # type: ignore[list-item] + config.token_source_priority[:] = ["query"] # type: ignore[list-item] token = _extract_header_token( _connection({"Authorization": "Bearer from-bearer"}), config @@ -466,7 +467,7 @@ def test_a_known_source_after_an_unknown_one_is_still_honoured( config: SessionConfig, ) -> None: """Falling through must continue the chain, not abandon it.""" - config.token_source_priority[:] = ["cookie", "header"] # type: ignore[list-item] + config.token_source_priority[:] = ["query", "header"] # type: ignore[list-item] token = _extract_header_token( _connection({config.header_name: "from-header"}), config diff --git a/tests/session/test_starlette_middleware.py b/tests/session/test_starlette_middleware.py index 09d07e9..ac4cf52 100644 --- a/tests/session/test_starlette_middleware.py +++ b/tests/session/test_starlette_middleware.py @@ -963,12 +963,13 @@ async def test_cookie_token_varies_on_cookie_and_the_headers_checked_first( def test_disabled_bearer_transport_is_left_out_of_vary(manager: SessionManager) -> None: - config = SessionConfig( - secret_key="a" * 32, - use_bearer_token=False, - cookie_name="session", - cookie_https_only=False, - ) + with pytest.warns(DeprecationWarning, match="use_bearer_token"): + config = SessionConfig( + secret_key="a" * 32, + use_bearer_token=False, + cookie_name="session", + cookie_https_only=False, + ) client = TestClient(_session_reading_app(manager, config)) response = client.get("/read")