Recovery-factor verification, factor-aware endpoints, and the cookbook#9
Merged
Conversation
…urable shapes in guides
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Recovery-factor verification, factor-aware endpoints, and the cookbook
This branch finishes the identity-and-recovery arc the model-driven identity contract started.
"Verified" and account recovery are now factor-agnostic, with email as the special case rather than
the concept, so a phone-recovery app verifies and resets over SMS through the built-in HTTP
endpoints. The public surface is renamed to stop saying "email" where it means "the recovery
factor," the verify/reset request bodies are shaped to the factor, and the release ships a
ten-recipe cookbook, an Identity API reference page, and refreshed architecture docs.
Recovery-factor verification
Before this branch, "verified" was hardcoded to email: the gate read
repo.email_verified(user),the column was
email_verified, andcurrent_user(verified=True)raised at construction unless themodel had an email column. The identity contract (already in
main) let an app declarerecovery="phone", but there was no way to actually prove control of that phone, so the recoveryfactor was only half real.
Now the repository resolves the factor.
recovery_verified(user)readsemail_verifiedfor anemail-recovery app and
{factor}_verifiedotherwise (and is alwaysFalsewhenrecovery=None);mark_recovery_verified(db, user)writes the matching column.Principalgains arecovery_verifiedfield that
build_principalfills, andcurrent_user(verified=True)gates on it, raising atconstruction only when the contract has no recovery factor (nothing to prove control of).
The proof machinery itself did not change. The signed, single-use, TTL'd token
(
create_signed_token/verify_signed_token/_consume) has a zero-line diff; the only changesin
confirm_*are the verified read and write switching to the factor-aware path, and on therequest side the delivery recipient is now
repo.get(user, recovery), which finally points thedelivery channel at the recovery factor instead of always the email.
Why: the contract already knows the recovery factor, so "verified" should mean that factor is
proven, not that an email is proven.
email_verifiedstays the column for email apps, and theconcept generalizes above it without a second source of truth.
The verified flag stays unsettable, on every write path
A non-email
{factor}_verifiedcolumn (e.g.phone_verified) is not aLOGICAL_FIELDSmember, soit had to be gated explicitly or it would be a settable-without-proof downgrade.
repo._recovery_verified_col()is unioned into both the register allowlist and the provisioning filter, so the flag is dropped on
all three write paths: registration (even if opted into
register_extra_fields),new_user_defaults(gated at construction), and
new_user_fields(gated throughfilter_provisioning_data). The onlyway to set it remains redeeming the delivered token, exactly as
email_verifiedalways behaved.Factor-aware recovery endpoints (phone recovery over HTTP)
The verify and reset flows generalized at the service layer, but the HTTP bodies of
/email/verify-requestand/password/reset-requeststill validatedEmailStr, so a phone appcould only drive recovery from Python, not over the built-in endpoints. That was the last open seam
in the arc.
The two request bodies are now generated from the recovery factor: an email-recovery app keeps
{"email": ...}validated as an address (byte-identical, so existing clients and the whole emailtest suite are unaffected), and a phone-recovery app gets
{"phone": ...}validated as a string.The change-email endpoints, which prove a real email address, now mount only when the model actually
has an
emailcolumn, so an email-less phone app no longer exposes broken change-email routes.Factor-neutral naming
The behavior generalized but the names had not, and one of them is on the public
AuthHookssurface.The verify delivery kind is now
verify_recoveryfor a non-email factor (email keepsverify_email,so existing
EmailSenderimplementations that switch on it are unaffected); the service methods arerequest_recovery_verification/confirm_recovery_verification; and the hookon_after_email_verifiedis nowon_after_recovery_verified. Names that are genuinelyemail-specific keep their names:
on_after_email_changedand OAuth both prove a real email, and the/verify-emailpath is user-facing config. These are real renames, not deprecated aliases, donewhile the project is pre-1.0 and nobody depends on the old names, so the breaking-change cost is at
its lowest.
Documentation: a cookbook, an identity reference, refreshed architecture
The identity-and-recovery work needed narrative docs, and the cookbook was the natural home for the
account-shapes story. This branch adds a Cookbook of ten from-scratch recipes (email + password,
username-only, phone recovery, sign in with Google, email + password + Google, a token API, web and
API in one backend, server-set fields, onboarding an existing users table, and going to production),
each ending on a principle with runnable code. It adds an Identity API reference page
(
make_auth_identity+IdentityConfig), refreshes the architecture page (the spine now listsidentity, plus a new identity-contract diagram regenerated through the SVG-to-PNG build script),and capitalizes the
CRUDAuthbrand in prose across all docs while leaving the lowercase packageidentifier in imports and install commands untouched.
Documentation Updates
docs/cookbook/: new section, ten recipes plus an index, wired into the nav.docs/api/identity.md: new reference page forIdentityConfigandmake_auth_identity.docs/architecture.md: added the "identity contract" section,identityin the spine, and anew
identity-contractdiagram; regenerated the architecture layer diagram.docs/guides/accounts/registration.md,docs/guides/auth/sessions.md: note the account shapeis configurable, so they no longer imply email is mandatory.
docs/guides/infra/hooks.md,docs/guides/accounts/email.md: the verify hook is nowon_after_recovery_verified.docs/changelog.md:0.3.0entry.readme-assets/build-diagrams.cjs: the newidentity-contractdiagram and theidentityspinefix, plus an arg filter to render a subset.
Test Plan
Automated
uv run ruff check crudauth tests— cleanuv run mypy crudauth tests --config-file pyproject.toml— clean (100 files)uv run pytest— 297 passinguv run --group docs zensical build— no issuesRecovery-factor verification
recovery_verifiedreads the right column per factor (email, phone,None)current_user(verified=True)passes when verified, 403s when not, and raises at construction forrecovery=None{factor}_verifiedfor a non-email factor, never double-emits for emailThe flag stays unsettable
{factor}_verifiedis dropped from registration,new_user_defaults, andnew_user_fieldsFactor-aware endpoints
{"phone": ...}; posting{"email": ...}is a 422Back-compat anchor
{"email": ...})Dependencies
None new. (
sharpis installed locally only to regenerate diagram PNGs from the build script; it isnot a package dependency.)
Breaking Changes
AuthHooks.on_after_email_verifiedrenamed toon_after_recovery_verified(and the internalrun_after_email_verifiedtorun_after_recovery_verified). An app registeringAuthHooks(on_after_email_verified=...)must rename the keyword.on_after_email_changedisunchanged.
EmailFlowService.request_email_verification/confirm_email_verificationrenamed torequest_recovery_verification/confirm_recovery_verification, and the verify and reset requestmethods now take a
valueparameter (the recovery-factor value) instead ofemail. The service isconstructed by
CRUDAuthinternally, so most apps are unaffected; direct callers must update.{factor}_verifiedcolumn (e.g.phone_verified). The appdeclares the factor column itself; the verified flag rides alongside it, set only by token redemption.