Session Management
Session management in this project is built around a flexible interface system that defaults to cryptographically signed, client-side cookies. This implementation ensures that session data is tamper-proof while remaining accessible across requests without requiring a server-side database by default.
Core Session Architecture
The session system is defined in src/flask/sessions.py and revolves around three main components: the session interface, the session object, and the state mixin.
Session Interface
The SessionInterface class defines the contract for how sessions are opened and saved. The default implementation is SecureCookieSessionInterface, which uses the itsdangerous library to sign session data.
Key methods in SessionInterface include:
open_session(app, request): Called at the start of a request to retrieve or create a session.save_session(app, session, response): Called at the end of a request to persist session changes (e.g., by setting a cookie).make_null_session(app): Creates aNullSessionif configuration (like a missing secret key) prevents normal session operation.
Session Objects and Mixins
Session objects, such as SecureCookieSession, inherit from SessionMixin. This mixin adds essential tracking properties to the session dictionary:
permanent: If true, the session cookie will have an expiration date defined byPERMANENT_SESSION_LIFETIME.modified: Automatically set toTruewhen the session dictionary is updated. This flag determines if aSet-Cookieheader is sent in the response.accessed: Set toTruewhenever a key is read from the session. When this is true, the response automatically includes aVary: Cookieheader to prevent caching issues.
# From src/flask/sessions.py
class SecureCookieSession(CallbackDict, SessionMixin):
def __init__(self, initial=None):
def on_update(self):
self.modified = True
super().__init__(initial, on_update)
The Session Lifecycle
Sessions are managed by the RequestContext in src/flask/ctx.py. When a request context is pushed, the application's session_interface is used to open the session:
# Logic from src/flask/ctx.py
self.session = self.app.session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = self.app.session_interface.make_null_session(self.app)
At the end of the request, during the response finalization in src/flask/app.py, the session is saved:
# Logic from src/flask/app.py
if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response)
Default Implementation: Signed Cookies
The SecureCookieSessionInterface uses itsdangerous.URLSafeTimedSerializer to sign the session data. It employs a specialized TaggedJSONSerializer (found in src/flask/json/tag.py) which supports extra types like UUID, datetime, and tuple that standard JSON cannot handle.
Secret Key Requirement
For the default interface to work, a SECRET_KEY must be configured. If it is missing, open_session returns None, and the system falls back to a NullSession. Attempting to write to a NullSession will raise a RuntimeError:
# Error raised by NullSession in src/flask/sessions.py
raise RuntimeError(
"The session is unavailable because no secret key was set. "
"Set the secret_key on the application to something unique and secret."
)
Customizing Session Behavior
You can replace the default session behavior by assigning a custom implementation to app.session_interface. This is commonly used to implement server-side sessions (e.g., using Redis or a database).
Example: Dynamic Cookie Names
The following example from tests/test_reqctx.py demonstrates extending the default interface to change the cookie name based on the request URL:
class PathAwareSessionInterface(SecureCookieSessionInterface):
def get_cookie_name(self, app):
if flask.request.url.endswith("dynamic_cookie"):
return "dynamic_cookie_name"
return super().get_cookie_name(app)
app = flask.Flask(__name__)
app.session_interface = PathAwareSessionInterface()
Accessing Routing Information
By default, open_session is called before the URL is matched to an endpoint. If your custom interface needs request.endpoint, you must manually trigger matching:
class MySessionInterface(SessionInterface):
def open_session(self, app, request):
# Manually match the request to get the endpoint
app_ctx = flask._app_ctx_stack.top
app_ctx.match_request()
assert request.endpoint is not None
# ... implementation ...
Configuration Options
The session system respects several configuration keys:
| Key | Description |
|---|---|
SECRET_KEY | The primary key for signing session cookies. |
SECRET_KEY_FALLBACKS | A list of old keys used to validate existing cookies during key rotation. |
SESSION_COOKIE_NAME | The name of the cookie (default: session). |
PERMANENT_SESSION_LIFETIME | A timedelta or integer seconds for permanent sessions. |
SESSION_REFRESH_EACH_REQUEST | If true, the cookie is sent on every request for permanent sessions. |
SESSION_COOKIE_HTTPONLY | Controls the HttpOnly flag on the cookie. |
SESSION_COOKIE_SECURE | Controls the Secure flag (requires HTTPS). |
SESSION_COOKIE_SAMESITE | Sets the SameSite attribute (Lax, Strict, or None). |
Important Considerations
Nested Mutable Objects
The modified flag is only triggered when the session dictionary itself is updated. If you store a mutable object like a list or another dictionary inside the session and modify it, you must manually set session.modified = True:
session["user_roles"] = ["admin"]
# Later in the request...
session["user_roles"].append("editor")
session.modified = True # Required to ensure the change is saved
The "Vary: Cookie" Header
Whenever the session is accessed (even just for reading), the accessed flag is set. The SessionInterface.save_session method checks this flag and adds Vary: Cookie to the response headers. This informs caching proxies that the response content may vary based on the user's session cookie.