Skip to main content

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 a NullSession if 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 by PERMANENT_SESSION_LIFETIME.
  • modified: Automatically set to True when the session dictionary is updated. This flag determines if a Set-Cookie header is sent in the response.
  • accessed: Set to True whenever a key is read from the session. When this is true, the response automatically includes a Vary: Cookie header 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).

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:

KeyDescription
SECRET_KEYThe primary key for signing session cookies.
SECRET_KEY_FALLBACKSA list of old keys used to validate existing cookies during key rotation.
SESSION_COOKIE_NAMEThe name of the cookie (default: session).
PERMANENT_SESSION_LIFETIMEA timedelta or integer seconds for permanent sessions.
SESSION_REFRESH_EACH_REQUESTIf true, the cookie is sent on every request for permanent sessions.
SESSION_COOKIE_HTTPONLYControls the HttpOnly flag on the cookie.
SESSION_COOKIE_SECUREControls the Secure flag (requires HTTPS).
SESSION_COOKIE_SAMESITESets 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

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.