diff --git a/docs/middleware.md b/docs/middleware.md index 68bcc5966..01dd1fd99 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -132,17 +132,19 @@ The following arguments are supported: * `path` - The path set for the session cookie. Defaults to `'/'`. * `https_only` - Indicate that Secure flag should be set (can be used with HTTPS only). Defaults to `False`. * `domain` - Domain of the cookie used to share cookie between subdomains or cross-domains. The browser defaults the domain to the same host that set the cookie, excluding subdomains ([reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#domain_attribute)). +* `digest_method` - Hash function to use when generating the HMAC signature for the session. This defaults to SHA1 (will fail on systems that enforce use of FIPS algorithms), but can be changed to any other function in the hashlib module. ([reference](https://docs.python.org/3/library/hashlib.html#hash-algorithms)) ```python from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.middleware.sessions import SessionMiddleware +import hashlib routes = ... middleware = [ - Middleware(SessionMiddleware, secret_key=..., https_only=True) + Middleware(SessionMiddleware, secret_key=..., https_only=True, digest_method=hashlib.sha256) ] app = Starlette(routes=routes, middleware=middleware) diff --git a/starlette/middleware/sessions.py b/starlette/middleware/sessions.py index 1b95db4b0..71bd68ab6 100644 --- a/starlette/middleware/sessions.py +++ b/starlette/middleware/sessions.py @@ -2,7 +2,7 @@ import json from base64 import b64decode, b64encode -from typing import Literal +from typing import Any, Literal import itsdangerous from itsdangerous.exc import BadSignature @@ -23,9 +23,11 @@ def __init__( same_site: Literal["lax", "strict", "none"] = "lax", https_only: bool = False, domain: str | None = None, + digest_method: Any | None = None, ) -> None: self.app = app - self.signer = itsdangerous.TimestampSigner(str(secret_key)) + self.digest_method = digest_method + self.signer = itsdangerous.TimestampSigner(str(secret_key), digest_method=self.digest_method) self.session_cookie = session_cookie self.max_age = max_age self.path = path diff --git a/tests/middleware/test_session.py b/tests/middleware/test_session.py index b4f3c64fa..ce4d1f7c6 100644 --- a/tests/middleware/test_session.py +++ b/tests/middleware/test_session.py @@ -1,3 +1,4 @@ +import hashlib import re from starlette.applications import Starlette @@ -58,6 +59,40 @@ def test_session(test_client_factory: TestClientFactory) -> None: assert response.json() == {"session": {}} +def test_session_sha256(test_client_factory: TestClientFactory) -> None: + """test session with sha256 signed session""" + app = Starlette( + routes=[ + Route("/view_session", endpoint=view_session), + Route("/update_session", endpoint=update_session, methods=["POST"]), + Route("/clear_session", endpoint=clear_session, methods=["POST"]), + ], + middleware=[Middleware(SessionMiddleware, secret_key="example", digest_method=hashlib.sha256)], + ) + client = test_client_factory(app) + + response = client.get("/view_session") + assert response.json() == {"session": {}} + + response = client.post("/update_session", json={"some": "data"}) + assert response.json() == {"session": {"some": "data"}} + + # check cookie max-age + set_cookie = response.headers["set-cookie"] + max_age_matches = re.search(r"; Max-Age=([0-9]+);", set_cookie) + assert max_age_matches is not None + assert int(max_age_matches[1]) == 14 * 24 * 3600 + + response = client.get("/view_session") + assert response.json() == {"session": {"some": "data"}} + + response = client.post("/clear_session") + assert response.json() == {"session": {}} + + response = client.get("/view_session") + assert response.json() == {"session": {}} + + def test_session_expires(test_client_factory: TestClientFactory) -> None: app = Starlette( routes=[