Session Management¶
The pyfly.session module provides server-side HTTP session management with a
pluggable store backend. It mirrors the Spring Session model: a SessionFilter
reads a session cookie on every request, loads (or creates) an HttpSession
from a SessionStore, attaches it to request.state.session, and persists
changes after the response. Three stores ship out of the box — in-memory for
development, and Redis or a SQL table (any relational backend) for production.
Quick Example¶
from pyfly.session import HttpSession, SessionFilter, SessionStore
from pyfly.session.adapters.memory import InMemorySessionStore
store = InMemorySessionStore()
filter_ = SessionFilter(store=store, cookie_name="PYFLY_SESSION", ttl=1800)
# Inside a request handler, once the filter has run:
# session = request.state.session # HttpSession instance
# session.set_attribute("user_id", "alice")
# session.get_attribute("user_id") # "alice"
# session.invalidate() # marks for deletion
Configuration¶
Enable sessions in pyfly.yaml. Auto-configuration wires the store and filter
automatically:
pyfly:
session:
enabled: true
store: memory # memory (default) | redis | postgres
cookie-name: PYFLY_SESSION # default
ttl: 1800 # seconds (default: 30 minutes)
cookie:
secure: false # set true in production (HTTPS only)
redis:
url: redis://localhost:6379/0
| Key | Default | Description |
|---|---|---|
pyfly.session.enabled |
— | Must be true to activate session support |
pyfly.session.store |
memory |
Store backend: memory, redis or postgres (case-insensitive); any other value raises ValueError at startup |
pyfly.session.cookie-name |
PYFLY_SESSION |
Name of the session cookie |
pyfly.session.ttl |
1800 |
Session lifetime in seconds |
pyfly.session.cookie.secure |
false |
Set true to mark the cookie Secure (HTTPS only) |
pyfly.session.redis.url |
redis://localhost:6379/0 |
Redis connection URL (used when store=redis) |
pyfly.session.postgres.datasource |
primary | The datasource of the SQL store (store=postgres) |
pyfly.session.postgres.url |
— | Alias resolved through the DataSourceRegistry: the registered datasource with that URL, or a new session datasource |
The redis store requires redis.asyncio to be installed
(pip install redis). If it is not available, the auto-configuration falls
back to the in-memory store and logs a session_store_fallback WARNING.
Before 26.09.08 any
storevalue other thanredis(jdbc,postgres, a typo) silently became the in-memory store.
Key APIs¶
HttpSession¶
HttpSession wraps the session data dictionary with typed accessors and tracks
mutation state so the filter knows when to persist.
| Property / Method | Description |
|---|---|
id |
Unique session identifier (UUID hex string) |
is_new |
True if the session was created during this request |
stored_id |
The id the store is known to hold the session under: the id it was loaded with, then the id of its last save (None for a new session not saved yet) |
rotate_id(*, on_login=False) |
Assign a fresh id (session-fixation defense). on_login=True for a login: the new id is stored even if the old entry is gone meanwhile. Otherwise (a privilege elevation) the filter moves the stored session to the new id only while the store still holds it, so a session logged out or evicted meanwhile is not brought back |
previous_id |
The id before the last rotate_id() |
rotated_on_login |
True when rotate_id(on_login=True) was called since the session was last persisted |
created_at |
Unix timestamp of session creation (float) |
last_accessed |
Unix timestamp of the most recent access (float) |
invalidated |
True if invalidate() has been called |
modified |
True if any attribute was set or removed (or session is new) |
get_attribute(name) |
Return attribute value or None |
set_attribute(name, value) |
Set an attribute; marks session as modified |
remove_attribute(name) |
Remove an attribute if present |
get_attribute_names() |
List of all user-set attribute names (excludes internal _* keys) |
invalidate() |
Mark the session for deletion; filter will delete cookie and store entry |
mark_persisted() |
Record that the store holds the session as it is now, under its current id: modified is False until the next change, stored_id is the current id and rotated_on_login is False (the filter calls it after each save; previous_id is kept) |
get_data() |
Raw session dict (includes internal metadata) |
SessionStore protocol¶
All session backends implement this runtime_checkable Protocol:
class SessionStore(Protocol):
async def get(self, session_id: str) -> dict[str, Any] | None: ...
async def save(self, session_id: str, data: dict[str, Any], ttl: int) -> None: ...
async def delete(self, session_id: str) -> None: ...
async def exists(self, session_id: str) -> bool: ...
save inserts or replaces. A store that can also write over a session, or move it to a new id, only while it
holds it is a ConditionalSessionStore (from pyfly.session import ConditionalSessionStore), with two more
methods:
class ConditionalSessionStore(SessionStore, Protocol):
async def replace(self, session_id: str, data: dict[str, Any], ttl: int) -> bool: ...
async def rename(self, old_id: str, new_id: str, data: dict[str, Any], ttl: int) -> bool: ...
replace writes the data and moves the expiry ttl seconds on, and rename moves the session to a new id with
the data and the new expiry, only if the store holds the session and it has not expired, in one atomic step;
each returns False (writing nothing) otherwise. The three shipped stores implement them: the in-memory store
under its lock, Redis with SET ... XX and a Lua script (both keys of a rename must be on one node: in a Redis
Cluster, in one hash slot), the SQL store with one conditional UPDATE each (a rename updates the key in place,
as Spring Session JDBC does). The SessionFilter uses it so that a request never brings back a session that was logged out,
evicted or expired while the request ran (see SessionFilter). A custom store without
replace keeps working: every change goes through save, and such a store cannot tell a revoked session from a
live one: the SessionFilter logs session_store_without_replace (a WARNING) when it is built on such a store.
A subclass of a shipped store that overrides save (to encrypt the data, say) must override replace and
rename the same way: the filter writes every change of a session the store already holds through them,
bypassing save.
InMemorySessionStore¶
Thread-safe in-memory store with TTL-based expiry. Uses asyncio.Lock.
Suitable for development, testing, and single-process deployments. Data is
lost on restart.
RedisSessionStore¶
Redis-backed store. Values are JSON-serialized; dataclass attributes (such as
SecurityContext) are round-tripped via a type-tag mechanism so OAuth2 session
login persists correctly. Keys are prefixed with pyfly:session:.
import redis.asyncio as aioredis
from pyfly.session.adapters.redis import RedisSessionStore
client = aioredis.from_url("redis://localhost:6379/0")
store = RedisSessionStore(client=client)
SqlSessionStore¶
A session store in the application's relational database (store=postgres), Spring Session JDBC's
place: every instance shares the sessions with no Redis. It runs on every backend SQLAlchemy supports
(PostgreSQL, MySQL, MariaDB, SQLite) on the framework table pyfly_sessions (session_id, the attributes
as JSON with the Redis store's type tags, expires_at as a UTC instant, indexed).
SqlSessionStore(engine_factory, *, table="pyfly_sessions", create_table=True, purge_interval=timedelta(seconds=60), clock=None):
engine_factory is the datasource (an AsyncEngine, a registry DataSource, a datasource name, or a
callable returning one). start() creates the table when the schema strategy allows it
(pyfly.data.relational.ddl-auto), otherwise checks it and fails fast. A session is read only before it
expires; a write purges a batch of expired sessions at most once per purge_interval, after its commit,
and purge_expired() deletes them all. Each operation joins the unit of work bound for the store's
datasource, and outside one is a single statement.
SessionFilter¶
An OncePerRequestFilter ordered at HIGHEST_PRECEDENCE + 150. It runs
before authentication filters so the session is available when
OAuth2SessionSecurityFilter (HP+225) reads request.state.session.
| Constructor parameter | Default | Description |
|---|---|---|
store |
required | SessionStore instance |
cookie_name |
PYFLY_SESSION |
Session cookie name |
ttl |
1800 |
Session TTL in seconds |
secure |
False |
Whether to set the Secure cookie flag |
Cookie properties set by the filter:
| Property | Value | Reason |
|---|---|---|
httponly |
True |
Prevents JavaScript access (XSS mitigation) |
samesite |
lax |
Blocks cross-site request forgery for most flows |
secure |
configurable | Should be True in production |
max_age |
ttl |
Slides forward on every request (rolling TTL) |
On invalidation, the filter deletes the cookie and removes the store entry.
What the filter saves. A new session, and a session changed through the HttpSession API
(set_attribute, remove_attribute, rotate_id; invalidate deletes it). A value mutated in place (an item
appended to a list attribute, say) is saved only along with such a change: once the session was saved, an
in-place mutation alone is not saved again. Call set_attribute with the mutated value to save it.
A revoked session stays revoked. A session the store is known to hold (stored_id: loaded by the request,
or already saved by it) is written back through the store's replace, and moved to its new id through rename
when the request rotated it (rotate_id(), a privilege elevation), only while the store still holds it.
When the session was logged out (by another request of the same browser), evicted (by a login elsewhere under
evict-oldest) or expired while the request ran, its change is dropped, the session counts as invalidated and
the response sets no session cookie at all: the request neither brings the session back nor sends its cookie
again, and it does not clear the cookie either, since another request of the same browser (a login in another
tab, rotating the session) may have set a new one meanwhile, and it does not bring the session back under a new
id. A new session, and one rotated by a login (rotate_id(on_login=True): a fresh authentication stands even
if the old entry is gone), is inserted with save, and the entry the store held the session under
(stored_id) is deleted, however many rotations came before, so no earlier id resolves to the session. With a
custom store that has no replace and rename, every change goes through save, which brings such a session
back.
Deletions run to their end. The deletion of an invalidated session (a logout, a refused or failed login)
and of a rotated session's old id runs in a task of its own, shielded from the request's cancellation: a
level-triggered cancel scope, as anyio's, cancels every await of the request's cleanup too, and a logout
cancelled that way (the client went away) left the session live. A deletion that fails after its request was
cancelled is logged as session_delete_failed.
request.state.persist_session (a coroutine function taking no arguments) saves the session at once. Every
save leaves the session unmodified (mark_persisted()), so the persist that runs when the handler returns
writes it again only for a later change, and then only while the store still holds it. The OAuth2 login
handler relies on it: it saves the session it logs in before registering it, and a concurrent login that evicts
that session meanwhile is not undone when the login's request ends, even if the application changes the
session after the handler returned (the change is dropped with the session).
Auto-Configuration¶
Two auto-configuration classes activate when pyfly.session.enabled=true:
| Class | Bean | Condition |
|---|---|---|
SessionStoreAutoConfiguration |
session_store |
SessionStore bean not already present |
SessionFilterAutoConfiguration |
session_filter |
always (when enabled) |
A third class, SessionConcurrencyAutoConfiguration, activates independently
when pyfly.session.concurrency.enabled=true — see
Concurrency Control.
SessionStoreAutoConfiguration checks pyfly.session.store:
redis→RedisSessionStore(requiresredis.asyncio; falls back to memory, with a WARNING, if unavailable)postgres→SqlSessionStoreon the datasourcepyfly.session.postgres.*names (the primary by default), resolved in the context'sDataSourceRegistrymemory→InMemorySessionStore; any other value raisesValueError
Provide your own SessionStore bean to bypass auto-configuration entirely.
Integration with OAuth2 Login¶
OAuth2LoginHandler writes the authenticated SecurityContext into the
session under the key SECURITY_CONTEXT. On subsequent requests,
OAuth2SessionSecurityFilter (ordered at HP+225, after SessionFilter at
HP+150) reads this attribute and restores the SecurityContext onto
request.state.security_context.
This means browser-based OAuth2 login works without any extra wiring: enable sessions, enable OAuth2 login, and the two filters cooperate automatically.
Concurrency Control¶
Mirroring Spring Security's maximumSessions, PyFly can cap the number of
concurrent sessions per authenticated principal. The cap is enforced at the
single point where a principal becomes bound to a session — OAuth2 login —
after the session id has been rotated. With no cap configured, the registry is
unused and behavior is unchanged.
Configuration¶
pyfly:
session:
concurrency:
enabled: true
max-sessions: 1 # -1 = unlimited (default)
strategy: evict-oldest # evict-oldest (default) | reject-new
| Key | Default | Description |
|---|---|---|
pyfly.session.concurrency.enabled |
— | Must be true to activate concurrency control |
pyfly.session.concurrency.max-sessions |
-1 |
Maximum live sessions per principal; -1 means unlimited |
pyfly.session.concurrency.strategy |
evict-oldest |
What to do when the cap is exceeded: evict-oldest or reject-new |
Strategies
evict-oldest— the new login succeeds; the oldest session(s) for that principal are removed from the registry and deleted from the session store. The registry commits the eviction first; the deletion then runs outside the caller's unit of work and is shielded from the login's cancellation (the controller'sstop()waits for deletions in flight, up toEVICTION_STOP_TIMEOUT, 30 seconds, and logs what is left assession_eviction_unfinished). A deletion that fails is logged assession_eviction_failedand the login goes on: that session is no longer counted and stays usable until it expires or is invalidated.reject-new— the new login is refused. The handler invalidates the pending session and responds with HTTP401and body{"error": "max_sessions", ...}.
Only live sessions count, and the cap holds under concurrency.
- A session counts toward the cap while the session store has it. At each login the controller drops the
registrations of the principal's sessions the store no longer has (expired, invalidated by the
application, lost in a restart), as Spring's
getAllSessions(principal, false)leaves expired ones out, so a user whose sessions ended without a logout is never locked out. The login handler saves the session it logs in (request.state.persist_session, set by theSessionFilter) before registering it, so a concurrent login never takes it for a dead one. The handler makes every change (the security context, the redirect it consumes) before that save; afterwards the filter writes the session only for a change made through theHttpSessionAPI, and only while the store still holds it (replace, seeSessionFilter). So a session a concurrent login evicted stays evicted, and so does one evicted or logged out while any other request of it was running. If the save or the registration fails, the handler invalidates the session and the filter deletes it, so no logged-in session is left that the cap does not count. A logout deregisters the session: the OAuth2 login handler's logout, and the genericLogoutFilterwhen a controller is configured. Both invalidate the session first: a deregistration that fails (the registry's database or Redis down) is logged assession_deregistration_failedand never undoes the logout, and the controller drops the registration left behind as dead. This needs a store that holds every session the registry counts: beside a cross-process registry (redisorpostgres), only a shared store (store=postgresorredis) does. With the in-memory store there, the auto-configuration does not give the store to the controller (it would take the other instances' live sessions for dead ones and admit logins over the cap), so every registered session counts until it logs out or is evicted (see Registry Backends). - Counting, evicting and registering is one atomic step of the registry
(
AtomicSessionRegistry.register_limited): one unit of work that holds the principal's row on SQL, one Lua script on Redis, one lock in memory. Concurrent logins of one principal, on one instance or several, never exceed the cap. A custom registry without that method is serialized per principal within the process. Logouts and the purge change SQL registrations without the principal's lock; on MariaDB, whose snapshot isolation refuses to evict a registration that changed after the login read it ("Record has changed since last read", error 1020), the login runs its unit again on a fresh read, up to three times, and then raisesOptimisticLockingFailureException. - The SQL and in-memory registries are purged when the controller has the store, whatever the cap (with
max-sessions
-1nothing else removes a registration but a logout): a registration comes due for a liveness check one session TTL after it was registered or renewed;controller.purge_expired()drops the due registrations whose session is gone and renews the others. A login also checks up toLOGIN_PURGE_BATCH(50) due registrations, at most once a minute per instance, and the next logins take the rest of a backlog; schedulepurge_expired()to keep a large registry clean without them.
Before 26.09.08 the cap was a list and a register in separate transactions (max-sessions=1 let concurrent logins all in), and no registration was ever removed but by logout or eviction: with
reject-new, a user whose sessions expired could never log in again. Withevict-oldest, the filter also saved each login's session again when its request ended, bringing back the sessions concurrent logins had evicted: eight concurrent logins under a cap of one left eight logged-in sessions. And any request that changed its session saved it back when it ended (an insert-or-replace), so a session logged out or evicted while one of its requests ran was live again, authenticated its cookie, and the response sent that cookie anew.
Registry Backends¶
The cap is enforced against a SessionRegistry — a per-principal index of live
session ids, kept separate from the SessionStore. Three backends ship out of
the box, selected by pyfly.session.concurrency.registry:
registry |
Implementation | Scope | Requirements |
|---|---|---|---|
memory (default) |
InMemorySessionRegistry |
Single process only | none |
redis |
RedisSessionRegistry |
Cross-process / multi-instance | redis.asyncio installed |
postgres |
PostgresSessionRegistry |
Cross-process / durable | a relational datasource (any SQL backend) |
memory— in-process index guarded by anasyncio.Lock(mirrorsInMemorySessionStore). Each app instance counts only its own sessions, so the cap is not enforced across multiple processes. Suitable for single-node deployments, development, and testing. State is lost on restart. Its registrations come due for a liveness check one session TTL (pyfly.session.ttl) after they were registered or renewed, so the purge drops those of sessions that ended without a logout.redis— a cross-process index shared by all app instances. Each principal's live sessions are stored in a Redis sorted set (score =created_at, member =session_id), solist_sessionsis naturally oldest-first. Requiresredis.asyncio; if it is unavailable the auto-configuration falls back to the in-memory registry and logs asession_registry_fallbackWARNING. The connection URL comes frompyfly.session.concurrency.redis.url, falling back topyfly.session.redis.url, thenredis://localhost:6379/0.postgres— a durable, queryable, cross-process index for relational-only deployments (no Redis required), on every backend SQLAlchemy supports. Registrations live in the framework tablepyfly_session_registrations(session_idPK,principal,created_at,expires_at: when the registration comes due for a liveness check), andpyfly_session_principalsholds one row per principal, which a capped login locks. A principal's row stays after its last session ends (the table grows with the number of principals who ever logged in under a cap, not with their logins): deleting it would race the next login that locks it. The tables are created when the controller starts if the schema strategy allows it, otherwise checked. The datasource is the onepyfly.session.concurrency.postgres.datasourcenames, or the onepyfly.session.concurrency.postgres.urlresolves to in the context'sDataSourceRegistry, or the primary. Registrations an earlier release wrote topyfly_session_registryare not read (drop that table).
Pair a cross-process registry (redis or postgres) with a shared session
store (store=postgres or redis). With the in-memory store, each instance
knows only its own sessions, and the auto-configuration logs a
session_registry_not_shared WARNING. The cap still counts every instance's
registrations, but:
- evicting a session another instance holds removes its registration and leaves the session usable on that instance;
- dead sessions are not dropped: the controller does not ask a process-local
store about the registrations (it would take the other instances' live
sessions for dead ones), so a session that ends without a logout (expired,
lost in a restart) counts until it logs out or is evicted (on Redis, at most
until the principal's set expires, its
ttlafter the last login). Withreject-new, such sessions can lock the principal out, as before 26.09.08.
Configuration (Registry Backend)¶
pyfly:
session:
concurrency:
enabled: true
max-sessions: 1
strategy: evict-oldest
registry: redis # memory (default) | redis | postgres
redis:
url: redis://localhost:6379/0 # optional; falls back to pyfly.session.redis.url
| Key | Default | Description |
|---|---|---|
pyfly.session.concurrency.registry |
memory |
Registry backend: memory, redis, or postgres (case-insensitive); any other value raises ValueError |
pyfly.session.concurrency.postgres.datasource |
primary | The datasource of the SQL registry |
pyfly.session.concurrency.postgres.url |
— | Alias resolved through the DataSourceRegistry (a new session-registry datasource for another URL) |
pyfly.session.concurrency.redis.url |
falls back to pyfly.session.redis.url, then redis://localhost:6379/0 |
Redis connection URL (used when registry=redis) |
Auto-Configuration¶
When pyfly.session.concurrency.enabled=true,
SessionConcurrencyAutoConfiguration registers a
SessionConcurrencyController bean backed by the registry selected via
pyfly.session.concurrency.registry (InMemorySessionRegistry by default).
The OAuth2 login auto-configuration resolves this bean (if present) and passes
it to OAuth2LoginHandler, so no manual wiring is required:
| Class | Bean | Condition |
|---|---|---|
SessionConcurrencyAutoConfiguration |
session_concurrency_controller |
pyfly.session.concurrency.enabled=true |
The Redis client and the datasource are obtained in the auto-configuration (the composition root) and injected into the adapters — the adapters never import their driver at module scope (hexagonal wiring).
The controller gets the SessionStore (to tell live sessions from dead ones)
and its delete as the session_deleter, so an evicted session is deleted from
whichever store backend is active; it ends for every instance when the store is
shared (Redis or SQL). Beside a redis or postgres registry, the in-memory
store is passed as the session_deleter only (see
Registry Backends). The controller is a lifecycle bean:
its start creates or checks the SQL registry's tables.
Key APIs¶
from pyfly.session import (
ConcurrencyControlPolicy,
InMemorySessionRegistry,
SessionConcurrencyController,
SessionRegistry,
)
ConcurrencyControlPolicy is a frozen dataclass holding the cap configuration:
| Field | Default | Description |
|---|---|---|
max_sessions |
-1 |
Cap per principal; -1 (negative) means unlimited |
strategy |
"evict-oldest" |
"evict-oldest" or "reject-new" |
SessionRegistry is a runtime_checkable Protocol — a per-principal index of
live session ids, kept separate from the SessionStore. It is also exported
from pyfly.session.ports:
from pyfly.session.ports import SessionRegistry
class SessionRegistry(Protocol):
async def register(self, principal: str, session_id: str, created_at: float) -> None: ...
async def deregister(self, principal: str, session_id: str) -> None: ...
async def list_sessions(self, principal: str) -> list[tuple[str, float]]: ... # oldest first
async def count(self, principal: str) -> int: ...
AtomicSessionRegistry adds the capped registration the controller uses when a
registry has it:
from pyfly.session.concurrency import AtomicSessionRegistry, SessionRegistration
class AtomicSessionRegistry(Protocol):
async def register_limited(
self, principal: str, session_id: str, created_at: float, *, max_sessions: int, evict_oldest: bool
) -> SessionRegistration: ... # SessionRegistration(accepted, evicted)
InMemorySessionRegistry(*, ttl=timedelta(seconds=1800), clock=None) is the
in-process implementation (guarded by an asyncio.Lock), the default used by
auto-configuration when registry=memory (with ttl=pyfly.session.ttl). Like
the SQL registry it is an ExpiringSessionRegistry: ttl (positive: anything
else raises ValueError) is how long a registration goes before its liveness
is checked again. Two cross-process implementations ship as adapters; both
have their driver/datasource injected by the composition root, and both
implement register_limited:
from pyfly.session.adapters.redis_registry import RedisSessionRegistry
from pyfly.session.adapters.postgres_registry import PostgresSessionRegistry
RedisSessionRegistry(client, *, key_prefix="pyfly:session:user:", ttl=86400)
stores each principal's sessions in a Redis sorted set (oldest-first by
created_at). The ttl (seconds) bounds orphan growth and slides forward on
each register. Its register_limited is one Lua script over the set. Used
when registry=redis.
PostgresSessionRegistry(engine_factory, *, table="pyfly_session_registrations", principals_table="pyfly_session_principals", ttl=timedelta(seconds=1800), create_table=True, clock=None)
stores sessions in SQL tables. engine_factory is the datasource (an
AsyncEngine, a registry DataSource, a datasource name, or a callable
returning one, resolved on first use); the table names are validated as SQL
identifiers; ttl (the session timeout, positive: anything else raises
ValueError) is how long a registration goes before its liveness is checked
again. Its operations never join a unit of work of the caller. On SQLite, which
allows one writer at a time, an operation called inside a unit of work that has
written on the registry's datasource raises IllegalTransactionStateError (its
own unit would wait for the caller's write lock): call it outside that unit, or
give the registry a datasource of its own. Used when registry=postgres.
You may still provide your own SessionRegistry bean to override the
auto-configured one entirely.
SessionConcurrencyController enforces the policy:
| Method | Description |
|---|---|
__init__(registry, policy, *, session_deleter=None, session_store=None, purge_interval=timedelta(seconds=60)) |
session_store tells live sessions from dead ones; session_deleter (by default the store's delete) is an async (session_id) -> None callable used to evict store entries |
on_login(principal, session_id, created_at) |
Under a cap (max-sessions >= 0), drops the principal's dead sessions, then registers the session atomically, enforcing the cap; with no cap, registers it. Returns False if rejected (reject-new), True otherwise. Register a session after saving it, and do not save it again (an eviction may have deleted it) |
on_logout(principal, session_id) |
Deregisters the session |
purge_expired() |
Drops the due registrations whose session is gone and renews the others (an ExpiringSessionRegistry: the SQL and in-memory registries); returns how many were dropped |
Constructing a controller manually:
from pyfly.session import (
ConcurrencyControlPolicy,
InMemorySessionRegistry,
SessionConcurrencyController,
)
from pyfly.session.adapters.memory import InMemorySessionStore
store = InMemorySessionStore()
controller = SessionConcurrencyController(
InMemorySessionRegistry(),
ConcurrencyControlPolicy(max_sessions=1, strategy="reject-new"),
session_store=store,
)
# allowed is False once the cap is exceeded under "reject-new"
allowed = await controller.on_login("alice", session_id="abc123", created_at=1717000000.0)
See Also¶
- Security — JWT authentication,
@securedecorator,OAuth2SessionSecurityFilter - Web Filters —
OncePerRequestFilter, filter ordering,WebFilterChainMiddleware