Changelog¶
Notable changes to CRUDAuth, newest first. CRUDAuth is pre-1.0, so minor versions may include breaking changes; those are called out explicitly.
0.6.0 - 2026-06-21¶
CRUDAuth as a toolbox. The hardened auth flows that used to live only inside the route handlers
are now reusable primitives, the wired services are all reachable off auth, and the building
blocks are exported from the package root - so you can hand-roll a login, mint a token in a
webhook, or skip the routes entirely without losing the hardening. Everything is additive.
Added¶
auth.authenticate_password(db, identifier, password, *, request)- the credential check behind/loginand/token(shared escalating lockout, timing-equalized verification, disabled-account check), now a callable primitive onCRUDAuthandAuthRuntime./loginand/tokenwere de-duplicated onto it.auth.issue_tokens(user, *, scopes=None)(andBearerTransport.issue_tokens) - the bearer issuance behind/token, with scope clamping and thetoken_versionepoch. Reach for it instead of barecreate_access_token, which skips both./tokenissues through it;/refreshshares the underlying access-token minting.auth.emails(theEmailFlowService) andauth.oauth(theOAuthAccountService) accessors,Nonewhen unconfigured - matchingauth.repo/auth.sessions/auth.sudo.- Building blocks exported from
crudauth:UserRepository,SessionManager,SudoManager,EmailFlowService,OAuthAccountService, and the password helpersget_password_hash,verify_password,is_unusable_password,make_unusable_password. (Raw token-mint functions stay unexported on purpose - useissue_tokensso the clamp and epoch come along.) - A "Use the building blocks" cookbook recipe, and an end-to-end test suite against real Postgres (testcontainers) covering the login / token / refresh / lockout / revocation paths.
No breaking changes.
0.5.0 - 2026-06-21¶
Account & device management. The session/device endpoints apps kept hand-writing are now opt-in built-in routes, plus an in-session password change. Everything is additive.
Added¶
- Session & CSRF management routes (
SessionTransport(management_routes=True), off by default):GET /sessions(device list),DELETE /sessions/{id}(revoke one, ownership-checked,404if not found or not yours),POST /logout-all(with?keep_current=true), andPOST /csrf/refresh(re-mint a lost CSRF cookie; self-heals; the deliberate non-current_userrecovery path). Thin handlers over the existingSessionManager; the three mutating ones enforce CSRF via the session transport. POST /change-password(always mounted): change a known password while signed in. The current password is the re-authentication; a successful change bumpstoken_versionand revokes the user's other sessions (keeping the current one), and fireson_after_password_changed.401on a wrong current password,400on an OAuth-only account (use/set-password).on_after_password_changedhook, distinct fromon_after_password_reset(the token flow).SessionInfois now exported (theGET /sessionsresponse model), and a flat Endpoints API-reference page maps every mountable route in one place.SessionManager.set_csrf_cookie(...)(the CSRF half ofset_session_cookies, reusable on its own).
0.4.0 - 2026-06-21¶
Custom email bodies. EmailSender.send now receives an EmailContext, so you render your own
branded HTML for the verify / reset / change emails instead of delivering crudauth's plain text.
Added¶
EmailContextonEmailSender.send: the sender now gets the assembledlink(the token embedded in the URL),kind,recipient, andexpires_in, so it can build a real HTML template without parsing the link out ofbody. The context carries crudauth-owned render data only - never the bare token, never user-controlled fields - so a sender that drops it into HTML can't be an XSS or credential-leak vector.context.linkis the same assembled URL as inbody(one source). Per-user personalization (Hi Alice) stays aDeliveryChannelconcern (it has thedbhandle and owns escaping).- Bundled library skill (
crudauth/.agents/skills/crudauth/): crudauth now ships an embedded library skill, so AI coding agents follow crudauth's actual conventions and gotchas (account shapes, gates, recovery, custom email bodies, production wiring) in sync with the installed version. It travels in the wheel; install it into your project's agent withuvx library-skills(add--claudefor Claude Code).
Breaking changes¶
EmailSender.sendgains a requiredcontextparameter. Add it to yoursendsignature (async def send(self, *, to, subject, body, kind, context)); behavior is unchanged becausebodyis still the pre-rendered plain-text fallback, so a sender that ignorescontextproduces the same email as before.
0.3.0 - 2026-06-20¶
Account shapes. CRUDAuth's identity and recovery are now read from your model instead of assumed to be email, so an app can log in by username, recover by phone, or hold no email at all, with the same flows and the same security. Plus pluggable delivery channels, a server-side provisioning seam, and a ten-recipe cookbook.
Added¶
- Model-driven identity contract (
make_auth_identity()+IdentityConfig): the account shape is read from the model and the intent (login order, recovery factor) is declared inIdentityConfig, validated against the model at construction. Username-only accounts (no email) and non-email recovery become configuration, not forks. - Recovery-factor verification: "verified" now means the contract's recovery factor is proven
controlled, with email as the special case. A phone-recovery app verifies and resets over SMS,
and
current_user(verified=True)gates on the recovery factor. The verify and reset request endpoints are shaped to the factor, so a phone app drives them with{"phone": ...}. - Pluggable delivery channels (
DeliveryChannelport,channels=[...]): recovery tokens route over email, SMS, push, or any medium you implement; email is a built-in channel and every channel fires best-effort. - Provisioning seam (
new_user_fields/new_user_defaults): set app-owned columns on new users from a server-built context, on both/registerand OAuth signup, gated so a client can't reach a privileged column. - A Cookbook of ten from-scratch recipes (the three account shapes, OAuth, token APIs, existing-table onboarding, production), an Identity API reference page, and a refreshed architecture page.
Changed¶
current_user(verified=True)gates onrecovery_verified(which equalsemail_verifiedfor an email-recovery app) and raises at construction when the contract has no recovery factor.- The recovery
verify/resetrequest bodies are generated for the recovery factor; the change-email endpoints mount only when the model has anemailcolumn. - A non-email recovery factor emits a
{factor}_verifiedbookkeeping column (e.g.phone_verified) alongside the app-declared factor column.
Breaking changes¶
AuthHooks.on_after_email_verified→on_after_recovery_verified. The verification hook is factor-neutral now;on_after_email_changedkeeps its name (it proves a real email). Apps registering the old hook must rename it.EmailFlowService.request_email_verification/confirm_email_verification→request_recovery_verification/confirm_recovery_verification, and the verify / reset request methods take a factorvalueinstead ofemail. The service is constructed internally, so most apps are unaffected; direct callers must update.- Login resolves against the contract's
loginfields, replacing the@-in-identifier heuristic. The default (email + username) behaves the same.
0.2.1 - 2026-06-15¶
Changed¶
crudauth.__version__is now read from the installed package metadata rather than hardcoded, so it can't drift frompyproject.toml.
0.2.0 - 2026-06-15¶
A security and correctness pass over the extracted code, plus two capabilities the review surfaced as missing. Pre-beta, so fixes were made directly rather than behind shims.
Added¶
- Sudo mode (
sudo=SudoConfig()+auth.require_sudo()): short-lived re-authentication for sensitive actions, stamped on the session, with its own lockout and anon_after_sudohook. POST /set-passwordfor OAuth-only accounts to establish a first password while authenticated.- Token revocation for bearer tokens via a
token_versionepoch, bumped on password reset. - Atomic storage primitives (
set_if_absent/get_and_delete) and an atomicincrement_and_refresh_ttlon the rate-limiter backend. - Startup warning when an in-memory backend is active under what is likely a multi-worker deployment.
Changed / fixed¶
- Login hardening: trusted-proxy IP resolution (
trusted_proxy_hops), lockout-key canonicalization, timing-equalized verification (closes a user-enumeration oracle), and a SHA-256 pre-hash so bcrypt no longer truncates at 72 bytes. - Escalating lockout now re-arms its round TTL atomically, and a new
on_login_successknob controls what a good login clears. - OAuth:
stateis bound to the initiating browser (blocks login CSRF), the redirect target is hardened against open redirects, callback failures degrade gracefully, and a missing{provider}_idcolumn fails fast at startup. - Email: verify / reset / change consume tokens through the atomic one-time primitives; trigger emails are best-effort; the "existing account" notice is throttled.
- Repackaged into feature slices (
register/is now a package) with a documented import-direction architecture, and the test suite was reorganized into source-mirroring subpackages.
Breaking changes¶
/registeris a strict allowlist. Model columns are dropped unless named inregister_extra_fields.- Email endpoints renamed:
/email/verify-request,/email/verify-confirm,/password/reset-request,/password/reset-confirm,/email/change-request,/email/change-confirm. token_versioncolumn added toAuthUserMixin; a persisted schema needs the migration (or acolumn_mapentry) before bearer-token revocation works.check=now denies onFalseinstead of ignoring the return value.- Disabled accounts return the generic
"Incorrect username or password"(was a distinct error). - Bearer scopes are clamped to a grantable ceiling; tokens can't self-grant scopes.
- Storage and rate-limiter ports gained required methods; custom backends must implement
set_if_absent/get_and_deleteandincrement_and_refresh_ttl. - Removed:
OAuthToken,SessionData.is_active, and the single-argumentget_client_ip(request)signature (nowget_client_ip(request, trusted_hops=0)).
0.1.0 - 2026-06-13¶
Added¶
- Initial release, extracted from FastroAI into a standalone, transport-agnostic authentication library for FastAPI.
- One
CRUDAuthobject wiring session and bearer transports to a singlePrincipal, OAuth (Google / GitHub / custom), email flows, login lockout, rate limiting, pluggable memory/redis backends, lifecycle hooks, and acolumn_mapover your own SQLAlchemy model.