Sessions¶
Sessions are CRUDAuth's default transport. With no transports= argument you get cookie
auth backed by a server-side session store, CSRF protection, and the /login, /logout,
/register, and /me routes:
from crudauth import CRUDAuth
auth = CRUDAuth(session=get_session, user_model=User, SECRET_KEY="change-me")
app.include_router(auth.router)
To configure it, pass a SessionTransport explicitly:
from crudauth import CRUDAuth, SessionTransport, CookieConfig
auth = CRUDAuth(
session=get_session, user_model=User, SECRET_KEY="change-me",
transports=[
SessionTransport(
backend="redis",
redis_url="redis://localhost:6379",
session_timeout_minutes=30,
max_sessions_per_user=5,
cookies=CookieConfig(secure=True, samesite="lax"),
),
],
)
The routes you get¶
| Method & path | What it does |
|---|---|
POST /register |
Create an account (email, username, password). |
POST /login |
Log in with a configured login field (username or email by default) + password; sets the cookies. |
POST /logout |
Terminate the current session and clear the cookies. |
GET /me |
Return the authenticated user's identity. |
POST /login is a form post and accepts a remember_me flag (see below). To gate your own
routes, see Protecting routes.
A login of your own¶
Need a custom login (a different form, an extra step)? Call auth.authenticate_password for the
hardened credential check (the shared lockout, timing-equalized verification, and the
disabled-account check) and establish the session yourself, instead of reassembling those by hand:
@app.post("/my-login")
async def my_login(body: LoginIn, request: Request, response: Response, db: DbDep):
user = await auth.authenticate_password(db, body.username, body.password, request=request)
sid, csrf = await auth.sessions.create_session(request, user_id=auth.repo.user_id(user))
auth.sessions.set_session_cookies(response, sid, csrf)
return {"csrf_token": csrf}
A wrong password raises UnauthorizedException, a tripped lockout RateLimitException - the same
responses /login gives. See Use the building blocks.
How a session works¶
On a successful POST /login, CRUDAuth:
- Verifies the credentials (with lockout and timing equalization).
- Creates a session record in the backend (in-memory or Redis).
- Generates a CSRF token bound to the session.
- Sets an
httponlysession_idcookie and a readablecsrf_tokencookie.
On later requests the session transport reads session_id, validates it against the backend,
slides its idle timeout forward, and (on unsafe methods) checks CSRF before returning the
Principal.
CSRF¶
CSRF is on by default and uses the synchronizer-token pattern. The csrf_token cookie is
not httponly so your frontend can read it and echo it back in the X-CSRF-Token header
on mutating requests (POST, PUT, PATCH, DELETE):
function csrfToken() {
return document.cookie.split("; ").find(c => c.startsWith("csrf_token="))?.split("=")[1];
}
await fetch("/account", {
method: "POST",
headers: { "X-CSRF-Token": csrfToken(), "Content-Type": "application/json" },
body: JSON.stringify({ ... }),
});
A mutating request with a missing or wrong header is rejected with 403. Safe methods
(GET, HEAD, OPTIONS) are exempt. You can disable CSRF with SessionTransport(csrf=False),
but don't unless something else terminates CSRF in front of crudauth.
Remember me¶
POST /login accepts a remember_me form field. When set, the session and its cookie get
the longer remember_me_days lifetime instead of the default idle window:
Cookie policy¶
CookieConfig controls the cookie attributes:
secure=True (the default) means the cookies are only sent over HTTPS. For local development
over plain HTTP, set secure=False so the browser will store them. Use samesite="none"
(with secure=True) only if your frontend is on a different site than your API.
Multi-device sessions¶
Each login is a separate server-side session, so you can build "manage devices" and "sign
out everywhere" on top of auth.sessions:
@app.get("/account/sessions")
async def list_sessions(user: Principal = Depends(auth.current_user())):
return await auth.sessions.list_for_user(user.user_id)
@app.post("/account/sessions/{session_id}/revoke")
async def revoke_session(session_id: str, user: Principal = Depends(auth.current_user())):
await auth.sessions.revoke(session_id, owner_id=user.user_id) # owner check prevents cross-user revoke
@app.post("/account/sign-out-everywhere")
async def sign_out_all(user: Principal = Depends(auth.current_user())):
await auth.sessions.revoke_all(user.user_id)
max_sessions_per_user caps how many concurrent sessions a user can have; the oldest is
evicted past the cap.
Backends and lifespan¶
The in-memory backend is per-process, which is fine for development but breaks under multiple workers. Use Redis in production, and open and close the connections in your app's lifespan:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
await auth.initialize()
yield
await auth.shutdown()
app = FastAPI(lifespan=lifespan)
The full set of knobs is on the
SessionManager reference.