sandloom.sdk.security¶
Token issuing, revocation, blocking, password hashing and key generation.
Public API 1.0 (spec §2.2, Appendix C).
from sandloom.sdk.security import <name>. Signatures in a terminal: sandloom docs <name>. Beginner names are the ones generated code and the examples use; any other name is advanced (mark its import line # sandloom: advanced <reason> in generated code and examples).
Advanced¶
| Name | Kind | Summary |
|---|---|---|
apply_pending_revocation |
async function | Apply a core.auth.tokens_revoked.v1 payload (durable applier body). |
block_user |
async function | Block user_id NOW: every authenticated request of theirs gets 401. |
ClaimsUnavailable |
exception | A CRITICAL claims contributor failed: the token must not be minted. |
create_access_token |
function | Create JWT access token via sandloom.core.auth (stable adapter). |
decode_token |
function | Decode JWT using sandloom.core.auth.tokens (stable adapter). |
decode_token_cached |
function | Per-request-cached JWT decode via sandloom.core.auth (stable adapter). |
ensure_token_not_revoked |
async function | Raise 401 when claims predate a revocation (503 on fail-closed outage). |
ensure_user_not_blocked |
async function | Refuse to authenticate a blocked user: 401 "Account blocked". |
generate_private_key |
function | A fresh private key for algorithm (sandloom keys generate). |
generate_x25519_key |
function | |
has_internal_access |
function | Non-raising variant: True when the caller may see internal details. |
is_user_blocked |
async function | Live read of the subject-state record (raises on a cache outage). |
password_hasher |
function | A fresh argon2id PasswordHasher with the CONFIGURED parameters (PASSWORD_HASH_PROFILE / _TIME_COST / _MEMORY_KIB / _PARALLELISM... |
private_pem |
function | |
public_pem |
function | The SubjectPublicKeyInfo PEM of a key (or of a private key's public half). |
rehash_password_if_needed |
async function | New hash when stored_hash was made with other argon2 parameters than the configured ones (stronger or weaker), else None. |
revoke_user_tokens |
async function | Invalidate every access token issued to user_id up to now. |
revoke_user_tokens_on_commit |
async function | Revoke user_id's access tokens AFTER the current UoW commits. |
run_password_hashing |
async function | Run an argon2 hash/verify fn(*args) on the BOUNDED password pool. |
SERVICE_PRINCIPAL_ID |
constant | |
subject_commit_check |
function | A before_commit check: is the request's bearer STILL valid now? |
unblock_user |
async function | Lift an emergency block (audited auth.user.unblocked, same rules). |
x25519_private_pem |
function | |
x25519_public_pem |
function |
Advanced names¶
apply_pending_revocation¶
async def apply_pending_revocation(payload: dict[str, Any]) -> None
Apply a core.auth.tokens_revoked.v1 payload (durable applier body).
Idempotent; raises when the mark cannot be written so the delivery is
retried. See sandloom.core.auth.revocation.apply_pending_revocation.
block_user¶
async def block_user(
user_id: Any,
*,
reason: str,
actor_id: Any = None,
database: str | None = None,
) -> None
Block user_id NOW: every authenticated request of theirs gets 401.
The blocked value of the subject-state record is read live on every
authenticated request, so it applies on the very next request in every
process — including tokens minted after the block. It stays until
unblock_user (no TTL) and outranks every revocation.
Must run inside a Unit of Work: the audit record
(auth.user.blocked, actor + reason), the immediate token revocation
and the event are all written BEFORE the mark, and the mark is the LAST
step — a failure of any earlier step leaves no mark behind the rolled-back
audit (no block happens unaudited). A cache outage raises
BlockUnavailableError (503) and the UoW rolls the audit back.
Call it as the last step of the request (the mark itself is not
transactional).
On commit it also ends the user's sessions, so lifting the block does not
revive them: every access token issued so far is revoked
(revoke_user_tokens_on_commit) and core.auth.user_blocked.v1 is
published (transactional outbox) — identity revokes the user's refresh
tokens on it, locally or in its own service.
ClaimsUnavailable¶
class ClaimsUnavailable(HTTPException)(owner: str) -> None
A CRITICAL claims contributor failed: the token must not be minted.
Surfaces as 503 (retry later) — never a token silently missing
security claims such as tenancy's tenant_ids allowlist.
create_access_token¶
def create_access_token(
*,
data: dict[str, Any],
expires_delta: int | None = None,
) -> str
Create JWT access token via sandloom.core.auth (stable adapter).
Import at call-time so test patches against security are honored.
decode_token¶
def decode_token(token: str) -> dict[str, Any] | None
Decode JWT using sandloom.core.auth.tokens (stable adapter).
Import at call-time so tests patching sandloom.core.auth.tokens.decode_token
take effect.
decode_token_cached¶
def decode_token_cached(request: Any, token: str) -> dict[str, Any] | None
Per-request-cached JWT decode via sandloom.core.auth (stable adapter).
Import at call-time so tests patching sandloom.core.auth.tokens.decode_token
take effect (the cache wrapper calls it through the module global).
ensure_token_not_revoked¶
async def ensure_token_not_revoked(request: Any, claims: dict[str, Any]) -> None
Raise 401 when claims predate a revocation (503 on fail-closed outage).
ensure_user_not_blocked¶
async def ensure_user_not_blocked(user_id: Any) -> None
Refuse to authenticate a blocked user: 401 "Account blocked".
For token ISSUERS (password / SSO / 2FA login, refresh-token exchange):
call it before minting or rotating anything — and before collecting the
token's claims. A live read of the subject-state record — never a cached
copy — so the token minted next in this context is stamped after any
revocation it saw. On a
cache outage AUTH_REVOCATION_FAIL_CLOSED decides, like the per-request
check: closed answers 503, open logs and lets the issuance proceed.
generate_private_key¶
def generate_private_key(algorithm: str) -> PrivateKey
A fresh private key for algorithm (sandloom keys generate).
generate_x25519_key¶
def generate_x25519_key() -> x25519.X25519PrivateKey
(no docstring)
has_internal_access¶
def has_internal_access(x_internal_token: str | None) -> bool
Non-raising variant: True when the caller may see internal details.
is_user_blocked¶
async def is_user_blocked(user_id: Any) -> bool
Live read of the subject-state record (raises on a cache outage).
password_hasher¶
def password_hasher() -> Any
A fresh argon2id PasswordHasher with the CONFIGURED parameters
(PASSWORD_HASH_PROFILE / _TIME_COST / _MEMORY_KIB /
_PARALLELISM; fixed 16-byte salt, 32-byte digest).
Every component that hashes passwords builds its hasher here, so registration, password change/reset and CLIs share one work factor.
private_pem¶
def private_pem(key: PrivateKey) -> str
(no docstring)
public_pem¶
def public_pem(key: PrivateKey | PublicKey) -> str
The SubjectPublicKeyInfo PEM of a key (or of a private key's public half).
rehash_password_if_needed¶
async def rehash_password_if_needed(
plain_password: str,
stored_hash: str,
*,
hasher: Any = None,
) -> str | None
New hash when stored_hash was made with other argon2 parameters
than the configured ones (stronger or weaker), else None.
Call only after a SUCCESSFUL verify and store the result in the same Unit
of Work. Never raises (a failure is logged + counted in
sandloom_password_rehash_total{outcome="failed"}) — see
sandloom.core.auth.hashing.rehash_password_if_needed.
revoke_user_tokens¶
async def revoke_user_tokens(user_id: Any, *, valid_after: int | None = None) -> bool
Invalidate every access token issued to user_id up to now.
Framework-wide: current_context / require(...) /
get_current_user_id / TenantScope reject an older token with 401.
See sandloom.core.auth.revocation.revoke_user_tokens.
revoke_user_tokens_on_commit¶
async def revoke_user_tokens_on_commit(
user_id: Any,
*,
reason: str,
valid_after: int | None = None,
database: str | None = None,
require_immediate: bool = False,
) -> int
Revoke user_id's access tokens AFTER the current UoW commits.
The single framework path for "this DB change invalidates the user's
sessions" (identity and tenancy both use it): skipped on rollback, retried
after commit, persisted durably in the UoW's outbox
(core.auth.tokens_revoked.v1) for the session owner's applier; returns
the fixed valid_after mark in whole seconds, rounded UP (the precise
mark has microsecond precision: a token issued at or before it is revoked).
require_immediate=True also writes the mark BEFORE commit and refuses
the change (RevocationUnavailableError, 503) when it cannot — for
changes whose revocation must hold the moment they commit.
See sandloom.core.auth.revocation.revoke_user_tokens_on_commit.
run_password_hashing¶
async def run_password_hashing(fn: Callable[..., _T], *args: Any) -> _T
Run an argon2 hash/verify fn(*args) on the BOUNDED password pool.
See sandloom.core.auth.hashing.run_password_hashing
(PASSWORD_HASH_CONCURRENCY): never the loop's default executor.
SERVICE_PRINCIPAL_ID¶
SERVICE_PRINCIPAL_ID: UUID
(no docstring)
subject_commit_check¶
def subject_commit_check(request: Request) -> Callable[[], Awaitable[None]]
A before_commit check: is the request's bearer STILL valid now?
Returns a no-argument coroutine function that re-reads the caller's
revocation and block marks LIVE (no request memo, no staleness budget) and
raises 401 (revoked / blocked) or 503 (fail-closed cache outage).
A request without a bearer gets a no-op (its guards decide).
Guarantee (ADR-0003 "Guarantees, precisely"): every change that
withdraws authority — a tenancy role change / removal, an identity role
revocation, block_user, a password change — writes the mark BEFORE it
commits (revoke_user_tokens_on_commit(require_immediate=True) / the
block mark) and reads nothing our action writes. A mark seen rolls our
transaction back; no mark seen means the history is SERIALIZABLE with our
action first. It is NOT strictly serializable / linearizable: a withdrawal
that writes its mark and commits between this read and our COMMIT is not
seen — the window is narrowed to read → COMMIT, not closed (a fence making
the withdrawal wait for in-flight actions would close it; not
implemented). On a revocation-store outage AUTH_REVOCATION_FAIL_CLOSED
decides: closed (the prod default) → 503; open (the default outside prod)
→ the check ACCEPTS, like the request-start check. Register it with
await before_commit(subject_commit_check(request)) inside a UoW you
open yourself; request routes are re-checked automatically (writes, a
GET whose unit wrote) or with at_commit().
unblock_user¶
async def unblock_user(
user_id: Any,
*,
reason: str,
actor_id: Any = None,
database: str | None = None,
) -> None
Lift an emergency block (audited auth.user.unblocked, same rules).
The blocked value is replaced only AFTER the UoW commits
(on_commit), by compare-and-set with after:<now + skew> (with the
revocation TTL) — never deleted, so every token issued before the unblock
stays revoked and a concurrent writer is never overwritten blindly. A
rolled-back unblock leaves the user blocked, never unblocked without an
audit row. Fail-safe: if the post-commit swap fails (cache outage) the
user stays blocked — logged as an error; run the unblock again.
x25519_private_pem¶
def x25519_private_pem(key: x25519.X25519PrivateKey) -> str
(no docstring)
x25519_public_pem¶
def x25519_public_pem(key: x25519.X25519PrivateKey | x25519.X25519PublicKey) -> str
(no docstring)