Skip to content

Utilities

Cross-cutting helpers: password hashing, email and identifier normalization, client-IP resolution, and display masking.

crudauth.utils.get_password_hash

get_password_hash(password: str) -> str

Hash a plaintext password with bcrypt (random salt per call).

The password is SHA-256 pre-hashed before bcrypt (see [_bcrypt_input][crudauth.utils._bcrypt_input]), so there is no effective length ceiling and no silent truncation.

crudauth.utils.verify_password

verify_password(
    plain_password: str, hashed_password: str
) -> bool

Verify a plaintext password against a bcrypt hash.

Returns False (rather than raising) when the stored hash is malformed, so a corrupted row produces a clean "invalid password" path instead of a 500 - which would both leak information and be a DoS lever.

crudauth.utils.dummy_verify_password

dummy_verify_password(plain_password: str) -> None

Run a throwaway bcrypt verification and discard the result.

Called on the user-not-found branch of login 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.

crudauth.utils.make_unusable_password

make_unusable_password() -> str

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

is_unusable_password(hashed_password: str) -> bool

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

canonical_email(email: str) -> str
canonical_email(email: None) -> None
canonical_email(email: str | None) -> str | None

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

canonical_identifier(identifier: str) -> str

Normalize a login identifier the same way the user lookup does.

Email identifiers are canonicalized (trim + lowercase) so that case variants of one address (v@x.com, V@x.com) collapse to a single rate-limit / lockout key; otherwise an attacker could reset the per-username counter just by varying the case while still hitting the same account. Usernames (no @) are left as-is, matching get_by_username which is case-sensitive.

crudauth.utils.mask_email

mask_email(email: str) -> str

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).

Example
mask_email("john@example.com")  # "j***@example.com"
mask_email("a@x.io")            # "a***@x.io"
mask_email("not-an-email")      # "***"

crudauth.utils.get_client_ip

get_client_ip(
    request: Request, trusted_hops: int = 0
) -> str

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 (default) ignores forwarding headers entirely and uses the socket peer - correct when the app is directly exposed. N trusts the N right-most X-Forwarded-For entries as your proxies and returns the entry just left of them (clamped to the left-most entry if the chain is shorter), which an attacker prepending fake values cannot reach.

0

Returns:

Type Description
str

The resolved client IP, or "unknown" if it cannot be determined.

Example
# App behind a single trusted reverse proxy (e.g. nginx, Caddy):
CRUDAuth(..., trusted_proxy_hops=1)