Sessions
Sessions are loaded before your code runs and saved after it returns, by the
StartSession middleware. Both drivers sign the cookie with APP_KEY, so a
tampered cookie is rejected rather than trusted.
Drivers
| Driver | Where the payload lives | Trade-off |
|---|---|---|
| cookie | In the signed cookie | No storage to set up; capped by cookie size, and readable by the client |
| file | storage/framework/sessions | Server-side invalidation; only a signed id travels |
SESSION_DRIVER=cookie
SESSION_LIFETIME=7200
SESSION_COOKIE=craft_session
SESSION_SAME_SITE=lax
SESSION_SECURE_COOKIE=false
Signed is not encrypted. Under the
cookiedriver the payload is base64-encoded JSON. The client cannot change it — the signature would fail — but it can read it. Keep secrets out of the session, or use thefiledriver.
Using the session
def show(self, request):
session = request.session()
session.put("cart", [1, 2, 3])
session.get("cart", [])
session.has("cart") # present and not None
session.exists("cart") # present, even if None
session.forget("cart")
session.pull("cart") # read then remove
session.all() # everything except internal keys
Dictionary access works too:
session["cart"] = [1, 2]
"cart" in session
Flash data
A flashed value is readable on the next request and then dropped — the usual way to carry a message across a redirect:
def store(self, request):
request.session().flash("status", "Post created.")
return self.redirect(route="posts.index")
@if(session('status'))
<div class="alert">{{ session('status') }}</div>
@endif
Keep it one request longer:
request.session().reflash()
CSRF tokens
Every session carries a token, created on first use:
request.session().token()
request.session().regenerate_token()
In templates, @csrf renders the hidden field. On verification, the token is
accepted only from the request body (_token) or the X-CSRF-TOKEN header —
never from the query string, where a crafted cross-site link could plant it.
See Security.
Session lifecycle
session.regenerate() # new id, same data — run this on login
session.invalidate() # new id, no data, new token — run this on logout
session.flush() # clear data, keep the session and its CSRF token
Auth.login() calls regenerate() for you, which closes session fixation: a
session id fixed before login stops being valid afterwards.
Under the file driver, saving a session whose id changed — via regenerate()
or invalidate() — also destroys the old session file, so a fixated cookie
cannot keep resurrecting the pre-rotation session server-side.
Reaching the session outside a request
View helpers use a context variable that StartSession publishes:
from craft.http.session import get_current_session
session = get_current_session() # None outside a request
Prefer request.session() where you have the request.
Authentication state
Authenticate middleware rehydrates the user from the session on each request.
The manager holds no state between requests:
auth = app.make("auth")
auth.reset() # clear this request's user, keep the session
auth.logout() # clear the user and forget it from the session
That distinction matters — logout() inside middleware would erase the key it
is about to read.
File driver maintenance
Expired files are removed on read, and you can sweep them:
from craft.http.session import FileSessionStore
store = FileSessionStore(app_key, "storage/framework/sessions")
store.gc() # delete expired files, returns how many
store.destroy(session_id) # invalidate one session server-side
Session ids are hashed before use as filenames, so a crafted id cannot escape the directory.
Testing
from starlette.testclient import TestClient
client = TestClient(asgi_app)
client.get("/counter") # cookie issued
client.get("/counter") # same session
other = TestClient(asgi_app) # a separate session