Sudo mode¶
A logged-in session proves who the user is, but not that they're still at the keyboard right now. Sudo mode adds a short re-authentication step in front of sensitive actions (close account, rotate keys, change billing), so a session left open can't be used to do real damage.
from crudauth import CRUDAuth, SessionTransport, SudoConfig
auth = CRUDAuth(
session=get_session, user_model=User, SECRET_KEY="change-me",
transports=[SessionTransport()],
sudo=SudoConfig(window_seconds=300, max_attempts=3, lockout_seconds=900),
)
Elevation is stamped on the server-side session, so it dies with the session (logout or
revocation clears it). That's why sudo requires a SessionTransport; configuring it without
one raises at startup.
Elevating¶
There's no built-in sudo route, because the prompt is part of your UI. Build a small endpoint that re-verifies the password and elevates the current session:
from pydantic import BaseModel
class SudoIn(BaseModel):
password: str
@app.post("/sudo")
async def sudo(body: SudoIn, request: Request, user: Principal = Depends(auth.current_user())):
until = await auth.sudo.elevate(user, body.password, request=request)
return {"elevated_until": until.isoformat()}
elevate() re-checks the password and stamps the session with an absolute expiry
(window_seconds from now). A wrong password raises 401; a non-session credential raises
403. With two-factor authentication, an enrolled user can pass an authenticator code
instead: elevate(user, code=body.code, db=db, request=request).
Gating an action¶
Add auth.require_sudo() to any route that needs a recent re-auth. It passes only while the
session holds an unexpired elevation, and returns 403 otherwise:
@app.post("/account/close")
async def close_account(
user: Principal = Depends(auth.current_user()),
_: Principal = Depends(auth.require_sudo()),
):
... # reached only within the sudo window
Lockout¶
Repeated wrong passwords trip a dedicated sudo lockout, keyed separately from the login
lockout so one can't mask the other. Once tripped, elevate() raises SudoLockoutError
(429 with a Retry-After header), and the elevation on the session that tripped it is cleared.
Other sessions keep an elevation they already hold until it expires. Attempts are counted before
the password is checked, so concurrent guesses can't exceed the limit.
Auditing¶
An on_after_sudo hook fires on every successful elevation, which is a good place to write
an audit record:
from crudauth import AuthHooks
async def on_sudo(user, *, request, context):
await audit_log("sudo", user_id=user["id"])
auth = CRUDAuth(..., hooks=AuthHooks(on_after_sudo=on_sudo))