Account & device management¶
Most apps grow a settings page: "where am I signed in," sign out a single device, sign out everywhere, change my password. crudauth ships these as opt-in routes, so you wire buttons to them instead of hand-writing session bookkeeping, CSRF refresh, and an in-session password change. The security parts (CSRF, ownership checks, session eviction) are already handled.
This recipe assumes the email and password base (a session transport, a mounted router).
1. Turn the routes on¶
The session and CSRF routes are opt-in; flip management_routes=True on the SessionTransport.
/change-password is always present (it's a core flow), so it needs no flag.
from crudauth import CRUDAuth, SessionTransport
from myapp.db import get_session
from myapp.models import User
auth = CRUDAuth(
session=get_session, user_model=User, SECRET_KEY="change-me",
transports=[SessionTransport(management_routes=True)],
)
app.include_router(auth.router)
That mounts GET /sessions, DELETE /sessions/{id}, POST /logout-all, and POST /csrf/refresh,
alongside the always-on POST /change-password.
2. The device list¶
GET /sessions returns one entry per active session, with parsed device info and a current flag
for the calling session, so a "your devices" table is a single fetch:
curl http://localhost:8000/sessions -b jar.txt
# [{"session_id":"...","device":{"browser":"Chrome","os":"macOS",...},
# "ip":"...","created_at":"...","last_activity":"...","current":true}, ...]
3. Revoke a device, or sign out everywhere¶
DELETE /sessions/{id} revokes one session; it's ownership-checked, so a user can only drop their
own, and an id that isn't theirs (or doesn't exist) is a 404 with no leak. POST /logout-all drops
them all, with ?keep_current=true for the common "sign out my other devices":
# revoke one device (CSRF header required on the unsafe verb)
curl -X DELETE http://localhost:8000/sessions/<id> -b jar.txt -H "X-CSRF-Token: <token>"
# sign out everywhere except here
curl -X POST "http://localhost:8000/logout-all?keep_current=true" -b jar.txt -H "X-CSRF-Token: <token>"
# {"detail": "Signed out of all sessions.", "revoked": 3}
Revoking your own current session (or /logout-all without keep_current) clears the session
cookies on the way out.
4. Change a password¶
POST /change-password verifies the current password and sets the new one. The current password is
the re-authentication, so there's no email round-trip:
curl -X POST http://localhost:8000/change-password -b jar.txt -H "X-CSRF-Token: <token>" \
-H "Content-Type: application/json" \
-d '{"current_password":"old-one","new_password":"a-new-strong-one"}'
A wrong current password is 401; an OAuth-only account (no usable password) is 400 and should use
/set-password. A
successful change is treated as a compromise response: it bumps token_version (evicting bearer
tokens) and revokes the user's other sessions, keeping the current one, and fires the
on_after_password_changed hook (a good place for a "your password was changed" email).
5. Self-heal a lost CSRF cookie¶
An SPA can lose its CSRF cookie (a cleared cookie jar, a stale tab) while the session cookie is still
valid, which would make every mutation fail. POST /csrf/refresh re-mints it:
curl -X POST http://localhost:8000/csrf/refresh -b jar.txt
# {"csrf_token": "..."} (+ a fresh CSRF cookie)
It deliberately doesn't require a CSRF header (that would defeat the recovery), resolves the session cookie directly, and self-heals: if the current CSRF cookie is already valid it returns it unchanged rather than rotating. An attacker can trigger it cross-origin but can't read the response (CORS), so they never learn the token.
Why this is safe to expose¶
Each route is thin: authenticate, validate, call an existing SessionManager method. The dangerous
parts are handled for you, not left to your handler. CSRF is enforced on the unsafe verbs because they
sit behind a session principal (the one recovery exception, /csrf/refresh, is documented above).
DELETE /sessions/{id} is ownership-checked and returns 404 for both "not found" and "not yours,"
so it can't be used to probe other users' session ids. And a password change evicts other credentials
while keeping the caller signed in. You wire the buttons; the invariants come with the routes.
Where to go next¶
- The manual approach (build your own routes over
auth.sessions): Devices & sessions. - The password flows in full: Passwords.
- Going to production (Redis-backed sessions so the device list is shared across workers): Going to production.