Skip to content

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.

SessionTransport(max_sessions_per_user=5)

Next: Storage & lifespan →