Skip to content
Menu

Documentation

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)