Conversation
…uth; export the toolbox surface
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.
CRUDAuth as a toolbox: reusable auth primitives, uniform service access, an exported surface
CRUDAuth was half a toolbox. The building blocks existed, but the hardened auth flows were trapped
inside route handlers, the wired services were exposed inconsistently, and none of the primitives
were discoverable from the package root. This branch closes that gap: the credential check and token
issuance become reusable methods (so a hand-written login or a webhook-minted token keeps the
hardening), every wired service is reachable off
auth, and the building blocks are exported. Italso lands an end-to-end suite against real Postgres that was the before/after anchor for the
refactor. Everything is additive; no public behavior changed.
Reusable password authentication and token issuance
The credential check (escalating lockout, timing-equalized verification, the disabled-account check,
the uniform non-enumerable error) was inlined and duplicated in both the session
/loginand thebearer
/tokenhandlers, and callable from nowhere. Token issuance (access plus refresh, scopeclamping, the
token_versionepoch) was likewise inlined in/token, with/refreshrepeating theaccess-token half.
This extracts two primitives.
authenticate_password(db, identifier, password, *, request)lives onAuthRuntimeand is surfaced on the facade asauth.authenticate_password; both/loginand/tokennow delegate to it, removing the duplication.auth.issue_tokens(user, *, scopes=None)(andBearerTransport.issue_tokens) is the issuance behind/token;/refreshshares the underlyingaccess-token minting through a new private helper. The route handlers are now thin: authenticate or
issue, then the transport-specific bits (cookies, hook).
Why: the most security-critical code in the library, verifying a password and minting a token, was
the one part you could not reuse. A hand-rolled login over the raw pieces would almost certainly get
lockout, timing-equalization, or non-enumeration wrong, and a token minted with bare
create_access_tokensilently skips the scope clamp and the revocation epoch. The primitives nowcarry the hardening, so reusing them is the safe path and the duplication is gone.
Uniform service access off
authauth.repo,auth.sessions, andauth.sudowere public, but theEmailFlowServicewas reachableonly as the private
auth._email_serviceand theOAuthAccountServicewas never stored at all. Atoolbox should surface its wired services the same way.
Adds
auth.emails(theEmailFlowService) andauth.oauth(theOAuthAccountService), eachreturning
Nonewhen the feature is not configured. So a custom route can trigger a password reset(
auth.emails.request_password_reset(...)) or reuse the OAuth linking rules(
auth.oauth.get_or_create_user(...)) without reaching into private attributes.An exported toolbox surface
crudauth.__all__carried the config, port, and exception types but none of the reusable primitives,so the toolbox could not be found from
import crudauthor autocomplete.Exports the service types (
UserRepository,SessionManager,SudoManager,EmailFlowService,OAuthAccountService) and the password helpers (get_password_hash,verify_password,is_unusable_password,make_unusable_password). The raw token-mint functions are deliberately leftunexported so
auth.issue_tokensstays the blessed path (it brings the clamp and epoch along). Thenewly public classes that lacked them gained class docstrings and
Example:blocks.End-to-end tests against real Postgres
The existing suite runs on in-memory SQLite. SQLite is lenient about types, so it hides the kind of
dialect bug Convention 2 warns about (coercing a token's string
subto an integer primary key).Adds a
tests/e2e/suite that spins up a real PostgreSQL via testcontainers and drives full userjourneys over HTTP: the session password lifecycle, bearer token plus refresh plus revocation, the
shared login lockout across
/loginand/token, device management, recovery verification, andregistration hardening. It was green before the refactor and stayed green after, which is what made
the extractions safe to land. It skips automatically when Docker is not available.
Documentation Updates
docs/cookbook/use-the-building-blocks.md(new): the à-la-carte tour, with a hand-rolled loginover
authenticate_password, a webhook token viaissue_tokens, the wired-services table, and the"when to use which" guidance.
Guide notes, each linking to the recipe:
guides/auth/bearer.md(issue_tokens),guides/auth/sessions.md(authenticate_password),guides/accounts/email.md(auth.emails),guides/auth/oauth.md(auth.oauth).crudauth/.agents/skills/crudauth/SKILL.md: a "Use the building blocks" section so the bundledlibrary skill steers agents to the hardened primitives.
The symbol-level API reference picks up the new methods, accessors, and exports automatically through
mkdocstrings.
Test Plan
Automated
uv run ruff check crudauth tests— cleanuv run mypy crudauth tests --config-file pyproject.toml— clean (106 files)uv run pytest— 329 passing (7 new toolbox tests, 6 new Postgres e2e journeys)uv run --group docs zensical build— no issues (internal links validated)Reusable primitives (
tests/test_toolbox.py)/my-loginoverauthenticate_passwordestablishes a working sessionissue_tokensmints a token that authenticates; refresh token is in the bodyissue_tokensclamps scopes tograntable_scopes; raises without aBearerTransportauth.emails/auth.oauthreturn the service when configured,NoneotherwiseBehavior preservation (
tests/e2e/, real Postgres)/me, refresh, then a password reset revokes the old token/loginand/tokenfor the same identityDependencies
No new runtime dependencies. The dev group gains
testcontainers[postgres]andasyncpgfor thee2e suite.
Breaking Changes
None. Every change is additive: two new facade methods, two new accessors, nine new exports, and a
new test suite. The
/login,/token, and/refreshhandlers were refactored to delegate to thenew primitives but their HTTP behavior is unchanged, which the Postgres e2e journeys verify.