Session management is a critical component of web applications, allowing you to store and retrieve user data across multiple requests. sillo provides a robust, flexible session management system that's easy to configure yet powerful enough for complex applications.
Session management is a critical component of web applications, allowing you to store and retrieve user data across multiple requests. sillo provides a robust, flexible session management system that’s easy to configure yet powerful enough for complex applications.
Basic Session Setup
Section titled “Basic Session Setup”Setting up sessions in your sillo application is straightforward:
from sillo import silloAppfrom sillo.session.middleware import SessionMiddleware
app = silloApp()
# Add the session middleware with secret_key passed directlyapp.use( SessionMiddleware( secret_key="your-secure-secret-key", session_cookie_name="sillo_session", cookie_path="/", cookie_domain=None, cookie_secure=True, cookie_httponly=True, cookie_samesite="lax", session_expiration_time=86400 # 24 hours ))With this minimal setup, sillo will use the default cookie-based session backend. Your routes can now access the session through the request object:
@app.get("/")async def index(request, response): # Access the session counter = request.session.get("counter", 0) counter += 1 request.session["counter"] = counter
return response.text(f"You've visited this page {counter} times")Session Configuration Options
Section titled “Session Configuration Options”sillo offers various configuration options for customizing session behavior:
from sillo import silloAppfrom sillo.session import SessionConfigfrom sillo.session.middleware import SessionMiddlewarefrom sillo.session.file import FileSessionManager
app = silloApp()
session_config = SessionConfig( session_cookie_name="sillo_session", cookie_path="/", cookie_domain=None, cookie_secure=True, cookie_httponly=True, cookie_samesite="lax", session_expiration_time=86400, # 24 hours manager=FileSessionManager, session_file_storage_path="sessions", session_file_name="session_")
app.use(SessionMiddleware(config=session_config, secret_key="secret-key"))Configuration Options Reference
Section titled “Configuration Options Reference”| Option | Description | Default |
|---|---|---|
session_cookie_name | Name of the cookie storing the session ID | "session_id" |
cookie_path | Path for which the cookie is valid | "/" |
cookie_domain | Domain for which the cookie is valid | None |
cookie_secure | Whether cookie should only be sent over HTTPS | False |
cookie_httponly | Whether cookie should be accessible via JavaScript | True |
cookie_samesite | SameSite attribute ("lax", "strict", or "none") | "lax" |
expiry | Session lifetime in seconds | 86400 (24 hours) |
manager | Session backend class | SignedSessionManager |
Basic Session Operations
Section titled “Basic Session Operations”@app.get("/session-demo")async def session_demo(request, response): # Get a value with default if not present user_id = request.session.get("user_id", None)
# Set a value request.session["last_visit"] = time.time()
# Check if a key exists if "preferences" in request.session: preferences = request.session["preferences"]
# Remove a key if "temporary_data" in request.session: del request.session["temporary_data"]
# Clear the entire session # request.session.clear()
return response.json({ "user_id": user_id, "session_keys": list(request.session.keys()) })Session Properties and Methods
Section titled “Session Properties and Methods”Sessions in sillo behave similar to dictionaries but with additional methods:
| Method/Property | Description |
|---|---|
session.get(key, default=None) | Get a value, returning default if not present |
session[key] = value | Set a session value |
key in session | Check if key exists in the session |
del session[key] | Delete a key from the session |
session.clear() | Remove all keys from the session |
session.keys() | Get all keys in the session |
session.items() | Get all key-value pairs in the session |
session.pop(key, default=None) | Get and remove a key, returning default if not present |
session.is_empty() | Check if session has no data |
session.modified | Whether session has been modified |
Session Expiration
Section titled “Session Expiration”By default, sessions expire after 24 hours (86400 seconds). You can customize this:
# Set global session expiration time using recommended approachsession_config = SessionConfig( session_expiration_time=3600 # 1 hour)app.use(SessionMiddleware(config=session_config))
# Or set per-session expiration time@app.post("/login")async def login(request, response): # Authenticate user... request.session["user_id"] = user.id
# Set this specific session to expire in 30 minutes request.session.set_expiry(1800)
return response.json({"success": True})Session Backends
Section titled “Session Backends”sillo supports multiple session backends to store session data. Each backend has different characteristics suitable for various use cases.
Signed Cookie Sessions (Default)
Section titled “Signed Cookie Sessions (Default)”The simplest session backend, storing the session data directly in a signed cookie:
# Using recommended approach for signed cookie sessionssession_config = SessionConfig( manager=SignedSessionManager)app.use(SessionMiddleware(config=session_config))Pros:
- No server-side storage required
- Works well in distributed environments
- Simple setup
Cons:
- Limited storage size (4KB cookie limit)
- Session data sent with every request
- Cannot be invalidated server-side
File-based Sessions
Section titled “File-based Sessions”Stores session data in files on the server filesystem:
# Using recommended approach for file-based sessionssession_config = SessionConfig( manager=FileSessionInterface, session_file_storage_path="sessions", # Directory to store session files session_file_name="session_" # Prefix for session files)app.use(SessionMiddleware(config=session_config))Pros:
- Unlimited session data size
- Sessions can be invalidated server-side
- Simple setup for single-server environments
Cons:
- Not suitable for distributed environments
- Requires filesystem access
- Needs cleanup of expired session files
Building Custom Session Backends
Section titled “Building Custom Session Backends”You can create custom session backends by implementing the BaseSessionInterface:
from sillo.session.base import BaseSessionInterface
class RedisSessionInterface(BaseSessionInterface): """Redis-backed session interface"""
def __init__(self, session_key=None): super().__init__(session_key) self.redis_client = redis.Redis()
async def load(self): """Load the session data from Redis""" if not self.session_key: return
data = self.redis_client.get(f"session:{self.session_key}") if data: self._data = json.loads(data)
async def save(self): """Save the session data to Redis""" if not self.session_key: self.session_key = self.generate_sid()
expiry = self.get_expiry_age() self.redis_client.setex( f"session:{self.session_key}", expiry, json.dumps(self._data) ) self.modified = False
def get_session_key(self): """Return the session key""" return self.session_keySession Security Best Practices
Section titled “Session Security Best Practices”Session management requires careful attention to security:
Generate a Strong Secret Key
Section titled “Generate a Strong Secret Key”import secrets
# Generate a secure random keysecret_key = secrets.token_hex(32)
# For production, store this in environment variablessecret_key = os.environ.get("SECRET_KEY")
app.use(SessionMiddleware(secret_key=secret_key))Enable Secure Cookies
Section titled “Enable Secure Cookies”# Using recommended approach for secure cookiessession_config = SessionConfig( cookie_secure=True, # Only send cookies over HTTPS cookie_httponly=True, # Prevent JavaScript access cookie_samesite="lax" # Mitigate CSRF attacks)app.use(SessionMiddleware(config=session_config))Use Appropriate Session Expiration
Section titled “Use Appropriate Session Expiration”# Short expiration for sensitive operations@app.post("/banking/transfer")async def transfer(request, response): # Verify authentication is recent auth_time = request.session.get("auth_time", 0) if time.time() - auth_time > 300: # 5 minutes return response.redirect("/re-authenticate")
# Process transfer...Implement Session Invalidation
Section titled “Implement Session Invalidation”@app.post("/logout")async def logout(request, response): # Clear session and remove cookie request.session.clear()
return response.redirect("/login")Practical Examples
Section titled “Practical Examples”Example 1: User Authentication Flow
Section titled “Example 1: User Authentication Flow”@app.post("/login")async def login(request, response): data = await request.form username = data.get("username") password = data.get("password")
# Authenticate user (pseudo-code) user = authenticate_user(username, password) if not user: return response.redirect("/login?error=invalid_credentials")
# Store user info in session request.session["user_id"] = user.id request.session["username"] = user.username request.session["auth_time"] = time.time() request.session["is_admin"] = user.is_admin
return response.redirect("/dashboard")
@app.get("/dashboard")async def dashboard(request, response): # Check if user is logged in if "user_id" not in request.session: return response.redirect("/login")
username = request.session["username"] return response.html(f"<h1>Welcome, {username}!</h1>")
@app.post("/logout")async def logout(request, response): request.session.clear() return response.redirect("/login?message=logged_out")Example 2: Shopping Cart
Section titled “Example 2: Shopping Cart”@app.get("/cart")async def view_cart(request, response): # Initialize cart if it doesn't exist cart = request.session.get("cart", {})
# Calculate total total = sum(item["price"] * item["quantity"] for item in cart.values())
return response.json({ "items": cart, "total": total })
@app.post("/cart/add/{product_id}")async def add_to_cart(request, response): product_id = request.path_params.product_id quantity = int(request.query_params.get("quantity", 1))
# Get product details (pseudo-code) product = get_product(product_id) if not product: return response.json({"error": "Product not found"}, status_code=404)
# Get or initialize cart cart = request.session.get("cart", {})
# Add or update product in cart if product_id in cart: cart[product_id]["quantity"] += quantity else: cart[product_id] = { "name": product.name, "price": product.price, "quantity": quantity }
# Save cart to session request.session["cart"] = cart
return response.json({"success": True, "cart": cart})
@app.post("/cart/clear")async def clear_cart(request, response): if "cart" in request.session: del request.session["cart"]
return response.json({"success": True})Example 3: Multi-step Form with Session Data
Section titled “Example 3: Multi-step Form with Session Data”@app.get("/wizard/step1")async def wizard_step1(request, response): # Initialize or get form data form_data = request.session.get("wizard_data", {})
return response.html_template("wizard/step1.html", form_data=form_data)
@app.post("/wizard/step1")async def wizard_step1_post(request, response): form_data = await request.form
# Validate form (pseudo-code) if not validate_step1(form_data): return response.redirect("/wizard/step1?error=invalid_data")
# Initialize wizard data if not exists wizard_data = request.session.get("wizard_data", {})
# Update with step 1 data wizard_data.update({ "name": form_data.get("name"), "email": form_data.get("email") })
# Save back to session request.session["wizard_data"] = wizard_data
# Proceed to next step return response.redirect("/wizard/step2")
@app.post("/wizard/complete")async def wizard_complete(request, response): # Get all wizard data wizard_data = request.session.get("wizard_data", {})
# Process the complete submission result = process_wizard_submission(wizard_data)
# Clear wizard data from session del request.session["wizard_data"]
return response.redirect(f"/wizard/success?id={result.id}")What belongs in a session
Section titled “What belongs in a session”A session should hold identity and a small amount of state that is genuinely per-user and per-browser: the user id, a CSRF token, a flash message, a partially completed multi-step form.
It should not hold a shopping cart of arbitrary size, a cached user object, search results, or anything you could re-derive from the database. Every one of those grows the session, and where the session lives decides what that growth costs.
Cookie-backed sessions put the data in the browser. There is no server storage and no lookup, and the limit is hard: roughly 4 KB per cookie including overhead, sent on every request to the domain. A 3 KB session is 3 KB of upload on every image request too.
Server-backed sessions store data server-side and put only an identifier in the cookie. Size is bounded by your storage, invalidation is immediate, and every request costs a lookup.
The decision is usually made for you by one requirement: if you need to revoke a session immediately — a logout that must take effect everywhere, or an admin disabling an account — you need server-side storage. A signed cookie remains valid until it expires no matter what you do.
Session security essentials
Section titled “Session security essentials”Regenerate the session id on privilege change. Logging in must issue a new id. Without it, an attacker who can set a victim’s session cookie before login shares the session after it — session fixation, and it is still common.
Set the cookie flags. HttpOnly keeps JavaScript out, which contains
the damage from an XSS. Secure keeps it off plaintext connections.
SameSite=Lax blocks the cross-site POST case. All three, always.
Expire on two clocks. An idle timeout closes abandoned sessions on shared machines; an absolute lifetime bounds how long a stolen session is useful. Neither alone is sufficient.
Never trust session contents to be current. A permission cached in a session at login is a permission the user keeps after you revoke it. Store the identity in the session and look up authorization fresh.
Sessions and multiple processes
Section titled “Sessions and multiple processes”A cookie-backed session works identically across processes because the data travels with the request. A server-backed session does not, unless the store is shared.
An in-memory session backend is per-process: with four workers, a user is logged in on one and logged out on the other three, and the symptom is “login randomly does not work”. Any deployment beyond a single process needs Redis or a database behind the session store.
The same applies to invalidation. Clearing a session in one process does nothing to the copy in another unless the store is shared.
Sessions versus tokens
Section titled “Sessions versus tokens”A cookie session is convenient for a browser application: the browser manages it, it survives navigation, and revocation is immediate with a server-side store. It brings CSRF exposure with it, which is why the two guides sit next to each other.
A bearer token suits API clients and mobile apps: nothing is automatic, which removes CSRF entirely, and the client controls storage. Revocation is the hard part — a stateless token stays valid until it expires unless you keep a denylist, which puts the state back.
Most applications end up with both: sessions for the browser, tokens for the API. That is fine, provided each endpoint is clear about which it accepts. An endpoint accepting either is an endpoint whose CSRF exposure depends on how the caller authenticated.
Rotating the signing key
Section titled “Rotating the signing key”A cookie-backed session is only as good as the key that signs it, and a leaked key means forgeable sessions. Rotating requires accepting the old key for verification while signing with the new one, for at least one session lifetime — otherwise every logged-in user is logged out at once.