mirror of https://github.com/pallets/flask.git
enable secret key rotation
This commit is contained in:
parent
7522c4bcdb
commit
e13373f838
|
@ -20,6 +20,9 @@ Unreleased
|
|||
- ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
|
||||
``load_dotenv`` loads default files in addition to a path unless
|
||||
``load_defaults=False`` is passed. :issue:`5628`
|
||||
- Support key rotation with the ``SECRET_KEY_FALLBACKS`` config, a list of old
|
||||
secret keys that can still be used for unsigning. Extensions will need to
|
||||
add support. :issue:`5621`
|
||||
|
||||
|
||||
Version 3.0.3
|
||||
|
|
|
@ -125,6 +125,22 @@ The following configuration values are used internally by Flask:
|
|||
|
||||
Default: ``None``
|
||||
|
||||
.. py:data:: SECRET_KEY_FALLBACKS
|
||||
|
||||
A list of old secret keys that can still be used for unsigning, most recent
|
||||
first. This allows a project to implement key rotation without invalidating
|
||||
active sessions or other recently-signed secrets.
|
||||
|
||||
Keys should be removed after an appropriate period of time, as checking each
|
||||
additional key adds some overhead.
|
||||
|
||||
Flask's built-in secure cookie session supports this. Extensions that use
|
||||
:data:`SECRET_KEY` may not support this yet.
|
||||
|
||||
Default: ``None``
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
.. py:data:: SESSION_COOKIE_NAME
|
||||
|
||||
The name of the session cookie. Can be changed in case you already have a
|
||||
|
|
|
@ -79,7 +79,7 @@ source = ["src", "*/site-packages"]
|
|||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
files = ["src/flask", "tests/typing"]
|
||||
files = ["src/flask", "tests/type_check"]
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
strict = true
|
||||
|
@ -95,7 +95,7 @@ ignore_missing_imports = true
|
|||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.9"
|
||||
include = ["src/flask", "tests/typing"]
|
||||
include = ["src/flask", "tests/type_check"]
|
||||
typeCheckingMode = "basic"
|
||||
|
||||
[tool.ruff]
|
||||
|
|
|
@ -180,6 +180,7 @@ class Flask(App):
|
|||
"TESTING": False,
|
||||
"PROPAGATE_EXCEPTIONS": None,
|
||||
"SECRET_KEY": None,
|
||||
"SECRET_KEY_FALLBACKS": None,
|
||||
"PERMANENT_SESSION_LIFETIME": timedelta(days=31),
|
||||
"USE_X_SENDFILE": False,
|
||||
"SERVER_NAME": None,
|
||||
|
|
|
@ -315,14 +315,20 @@ class SecureCookieSessionInterface(SessionInterface):
|
|||
def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
|
||||
if not app.secret_key:
|
||||
return None
|
||||
signer_kwargs = dict(
|
||||
key_derivation=self.key_derivation, digest_method=self.digest_method
|
||||
)
|
||||
|
||||
keys: list[str | bytes] = [app.secret_key]
|
||||
|
||||
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
|
||||
keys.extend(fallbacks)
|
||||
|
||||
return URLSafeTimedSerializer(
|
||||
app.secret_key,
|
||||
keys, # type: ignore[arg-type]
|
||||
salt=self.salt,
|
||||
serializer=self.serializer,
|
||||
signer_kwargs=signer_kwargs,
|
||||
signer_kwargs={
|
||||
"key_derivation": self.key_derivation,
|
||||
"digest_method": self.digest_method,
|
||||
},
|
||||
)
|
||||
|
||||
def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import gc
|
||||
import re
|
||||
import typing as t
|
||||
import uuid
|
||||
import warnings
|
||||
import weakref
|
||||
|
@ -369,6 +370,27 @@ def test_missing_session(app):
|
|||
expect_exception(flask.session.pop, "foo")
|
||||
|
||||
|
||||
def test_session_secret_key_fallbacks(app, client) -> None:
|
||||
@app.post("/")
|
||||
def set_session() -> str:
|
||||
flask.session["a"] = 1
|
||||
return ""
|
||||
|
||||
@app.get("/")
|
||||
def get_session() -> dict[str, t.Any]:
|
||||
return dict(flask.session)
|
||||
|
||||
# Set session with initial secret key
|
||||
client.post()
|
||||
assert client.get().json == {"a": 1}
|
||||
# Change secret key, session can't be loaded and appears empty
|
||||
app.secret_key = "new test key"
|
||||
assert client.get().json == {}
|
||||
# Add initial secret key as fallback, session can be loaded
|
||||
app.config["SECRET_KEY_FALLBACKS"] = ["test key"]
|
||||
assert client.get().json == {"a": 1}
|
||||
|
||||
|
||||
def test_session_expiration(app, client):
|
||||
permanent = True
|
||||
|
||||
|
|
Loading…
Reference in New Issue