Devices & sessions¶
Every login is a separate server-side session, so you can show users where they're signed in and let them sign out of one device or all of them.
Built-in routes (opt-in)¶
Pass management_routes=True to the SessionTransport and CRUDAuth mounts the device-management
routes for you:
auth = CRUDAuth(
session=get_session, user_model=User, SECRET_KEY="change-me",
transports=[SessionTransport(management_routes=True)],
)
app.include_router(auth.router)
| Route | What it does |
|---|---|
GET /sessions |
list the user's active sessions (SessionInfo[]; current flags the caller's) |
DELETE /sessions/{id} |
revoke one session (ownership-checked; 404 if not found or not yours) |
POST /logout-all |
revoke all sessions; ?keep_current=true keeps the calling one |
POST /csrf/refresh |
re-mint the CSRF cookie when it's lost but the session is still valid |
All but /csrf/refresh run behind a session principal, so the unsafe verbs are CSRF-protected
automatically. /csrf/refresh is the deliberate exception: it resolves the session cookie directly
(requiring a valid CSRF header to refresh CSRF would defeat the recovery purpose) and self-heals,
returning the existing token unchanged when it's already valid. The routes are opt-in (False by
default) because adding endpoints and a device list isn't universally wanted.
Rolling your own¶
If you need a different shape, CRUDAuth exposes the session manager as auth.sessions whenever a
SessionTransport is configured, and you build the endpoints your UI needs on top of it. The
examples below are routes you add to your own app.
List a user's sessions¶
list_for_user returns one entry per active session, with device info parsed from the user
agent and a flag marking the current one:
@app.get("/account/sessions")
async def sessions(request: Request, user: Principal = Depends(auth.current_user())):
current = request.cookies.get("session_id")
return await auth.sessions.list_for_user(user.user_id, current_session_id=current)
# [{ "session_id", "device", "ip", "created_at", "last_activity", "current" }, ...]
Revoke one session¶
revoke takes an owner id so a user can only revoke their own sessions:
@app.post("/account/sessions/{session_id}/revoke")
async def revoke(session_id: str, user: Principal = Depends(auth.current_user())):
ok = await auth.sessions.revoke(session_id, owner_id=user.user_id)
return {"revoked": ok}
If session_id doesn't belong to owner_id, nothing is revoked and it returns False.
Sign out everywhere¶
revoke_all drops every session for a user. Pass exclude to keep the current one, which is
the usual "sign out my other devices":
@app.post("/account/sign-out-others")
async def sign_out_others(request: Request, user: Principal = Depends(auth.current_user())):
current = request.cookies.get("session_id")
count = await auth.sessions.revoke_all(user.user_id, exclude=current)
return {"signed_out": count}
A password reset is a good place to call revoke_all so a compromised account can be fully
locked out.
The session cap¶
max_sessions_per_user (default 5) bounds how many concurrent sessions a user can hold. When
a new login would exceed the cap, the oldest session is evicted automatically.