Use the building blocks¶
Sometimes you don't want CRUDAuth to own your endpoints. You want its pieces — the hardened password check, token minting, the session manager, the user repository — behind your own routes, your own login, your own background jobs. CRUDAuth is built for that: the facade is a composition root that wires everything from your config, and then hands you the wired services. The routes are optional.
This recipe is the à-la-carte tour. It assumes the email + password base.
The model: build the facade, use its parts¶
auth = CRUDAuth(session=get_session, user_model=User, SECRET_KEY=..., transports=[...])
# Mount as much or as little as you want:
app.include_router(auth.session_router) # just /login, /logout
# app.include_router(auth.router) # ...or everything
# ...or nothing, and write your own routes over the pieces below.
What you get off auth:
| Reach for | What it is |
|---|---|
auth.repo |
the UserRepository: get_by_email, resolve_login, create, update, token_version, ... |
auth.sessions |
the SessionManager: create_session, revoke, revoke_all, CSRF, lockout |
auth.sudo |
the SudoManager: elevate, is_elevated |
auth.emails |
the EmailFlowService (or None): request_password_reset, reset_password, ... |
auth.oauth |
the OAuthAccountService (or None): get_or_create_user |
auth.current_user(...) |
the gate dependency — works on your routes |
auth.rate_limit(...) / auth.require_sudo() |
the other dependencies |
Plus the pure functions, exported from the package root: get_password_hash, verify_password,
is_unusable_password, make_unusable_password.
Your own login, with the real hardening¶
The one rule: for the auth-critical flows, don't reassemble the hardening from raw pieces — call
the primitive that carries it. auth.authenticate_password is the exact credential check behind
/login and /token: the shared escalating lockout, timing-equalized verification (no
user-enumeration oracle), and the disabled-account check. Hand-rolling those yourself is how
enumeration and lockout-bypass bugs happen.
@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 (401); a tripped lockout raises RateLimitException
(429) — the same responses the built-in route gives, because it's the same code.
Your own token, with the clamp and the epoch¶
auth.issue_tokens is the issuance behind /token: scopes are clamped to the bearer transport's
grantable_scopes (a caller can't self-grant) and both tokens carry the token_version epoch (so a
password reset revokes them). Reach for this instead of the raw create_access_token — the raw
function skips the clamp and the epoch, which is exactly what makes a hand-minted token a liability.
@app.post("/service-token")
async def service_token(
admin: Annotated[Principal, Depends(auth.current_user(superuser=True))],
db: DbDep,
):
robot = await auth.repo.get_by_email(db, "robot@example.com")
return auth.issue_tokens(robot, scopes=["read"]) # {"access_token", "token_type", "refresh_token"}
A protected route of your own¶
current_user() is just a dependency; it doesn't care whether you mounted CRUDAuth's routes.
@app.get("/whoami")
async def whoami(user: Annotated[Principal, Depends(auth.current_user())]):
return {"id": user.user_id, "via": user.transport}
Triggering recovery yourself¶
When auth.emails is configured it drives the verify/reset/change flows with the same token
mint/verify the endpoints use:
if auth.emails is not None:
await auth.emails.request_password_reset(db, email) # sends over your channel(s)
When to use which¶
- Mount the built-in routes for the standard flows — they're hardened and maintained.
- Use the building blocks for the bespoke parts: a custom login screen, a token minted in a webhook, a profile endpoint, an admin tool, a migration script. You get the wired services and the hardened primitives without CRUDAuth owning your URL space.
The line to remember: the hardening lives in authenticate_password and issue_tokens, not in the
routes. Use those two and the rest is just your code calling auth.repo / auth.sessions.
Where to go next¶
- The full surface, symbol by symbol: API reference (
CRUDAuth,UserRepository, Transports). - Device & session management as built-in routes: Account & device management.