Utilities¶
Cross-cutting helpers: password hashing, email and identifier normalization, safe redirect validation, client-IP resolution, and display masking.
crudauth.utils.normalize_password
¶
Unicode-normalize a password (NFKC) so every way of typing it hashes the same.
é typed as one precomposed code point and as e plus a combining accent
normalize to the same string, as NIST SP 800-63B recommends.
crudauth.utils.get_password_hash
¶
Hash a plaintext password with bcrypt (random salt per call).
The password is NFKC-normalized (see normalize_password) and SHA-256 pre-hashed before bcrypt (see [_bcrypt_input][crudauth.utils.hashing._bcrypt_input]), so there is no effective length ceiling and no silent truncation. This call blocks for the bcrypt work; use get_password_hash_async inside an async route.
crudauth.utils.get_password_hash_async
async
¶
get_password_hash in a worker thread, off the event loop.
crudauth.utils.verify_password
¶
Verify a plaintext password against a bcrypt hash.
The NFKC-normalized password is checked first, then the password as typed, so hashes created before normalization keep verifying.
Returns False (rather than raising) when the stored hash is missing,
empty, the unusable sentinel, or malformed, so a corrupted row produces a
clean "invalid password" path instead of a 500. Those cases still pay a full
bcrypt verification, so an account without a verifiable hash answers in the
same time as one with a real hash. This call blocks for the bcrypt work; use
verify_password_async inside an async
route.
crudauth.utils.verify_password_async
async
¶
verify_password in a worker thread, off the event loop.
crudauth.utils.verify_and_update_password
¶
verify_and_update_password(
plain_password: str, hashed_password: str | None
) -> tuple[bool, str | None]
Verify a password and return a replacement hash when the stored one predates normalization.
Verification is the same as verify_password.
Returns:
| Type | Description |
|---|---|
bool
|
|
str | None
|
password when the stored hash only matched the password as typed (a hash |
tuple[bool, str | None]
|
created before normalization), and |
crudauth.utils.verify_and_update_password_async
async
¶
verify_and_update_password_async(
plain_password: str, hashed_password: str | None
) -> tuple[bool, str | None]
verify_and_update_password in a worker thread, off the event loop.
crudauth.utils.dummy_verify_password
¶
Run a throwaway bcrypt verification and discard the result.
For a hand-written flow's user-not-found branch, so the absent-user path
pays the same bcrypt cost as the existing-user path; without it, a missing
account returns measurably faster and becomes a user-enumeration oracle.
verify_password with a None hash does
the same work.
crudauth.utils.make_unusable_password
¶
Return a sentinel that no input can ever verify against.
Used for OAuth-only accounts. The leading ! makes the value an invalid
bcrypt hash, so verify_password always returns False for it. The
random suffix makes every sentinel unique. Mirrors Django's
set_unusable_password.
crudauth.utils.is_unusable_password
¶
Whether hashed_password is the unusable sentinel (or empty).
True means the account has no real password set - an OAuth-only account
(see make_unusable_password, whose
sentinel starts with !, never a valid bcrypt hash).
crudauth.utils.canonical_email
¶
Normalize an email for storage/comparison (trim + lowercase).
Ensures a user created via Google as Foo@x.com can log in by password as
foo@x.com without surprises.
crudauth.utils.canonical_identifier
¶
Normalize a login identifier into its lockout key (trim + casefold).
Case variants of one identifier (v@x.com / V@x.com, bob / BOB)
collapse to a single key, so an attacker can't reset the per-username counter
by varying the case while a case-insensitive lookup or collation still
reaches the same account. A key coarser than the lookup only makes the
lockout stricter.
crudauth.utils.mask_email
¶
Mask an email for display: john@example.com -> j***@example.com.
A display helper for shoulder-surfing / casual logs - not a security
control (it's obfuscation, not a guarantee). Returns "***" when there's
no @; keeps only the first local-part character (so a single-char local
part can't leak more than that one character).
crudauth.utils.is_cross_site
¶
Whether the browser marked request as sent from another site (Sec-Fetch-Site).
A request without the header (an API client, an older browser) isn't cross-site.
crudauth.utils.get_client_ip
¶
Resolve the client IP with a trusted-proxy boundary.
X-Forwarded-For is client-controllable at its left end, so honoring it
blindly lets an attacker forge a fresh IP per request and slip every per-IP
rate limit and lockout. This function only consults the header when the app
declares how many trusted proxies sit in front of it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The incoming request. |
required |
trusted_hops
|
int
|
Number of trusted reverse proxies in front of the app.
|
0
|
Returns:
| Type | Description |
|---|---|
str
|
The resolved client IP, or |
crudauth.utils.client_ip_key
¶
The rate-limit and lockout key for a client IP.
An IPv4 address keys as itself. An IPv6 client keys by its /64 network,
the smallest block a single subscriber is normally assigned, so rotating
addresses inside one allocation can't mint fresh budgets. An IPv4-mapped IPv6
address keys as its IPv4 address. Anything that isn't an IP ("unknown")
is returned unchanged.
crudauth.utils.safe_redirect_path
¶
Return a safe same-origin redirect path, or default when rejected.
Only single-slash-rooted relative paths are accepted. Absolute URLs, protocol-relative URLs, backslashes, control characters, and values with a URL scheme or network location are rejected. Use this for client-supplied post-login or post-logout redirect targets to prevent open redirects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
target
|
str | None
|
The untrusted redirect target. |
required |
default
|
str
|
The fallback path returned for an unsafe or missing target. |
'/'
|