Security¶
Authentication, TLS, user management, and the FastAPI server framework.
Authentication¶
SCRAM-SHA-256 and PLAINTEXT client authentication handshake for the Haystack HTTP protocol.
Haystack authentication handshake.
Implements the Project Haystack authentication protocol including SCRAM-SHA-256 and PLAINTEXT mechanisms.
Uses the cryptography library for all key derivation, HMAC, and hashing.
See: https://project-haystack.org/doc/docHaystack/Auth
- async hs_py.auth.authenticate(session, base_url, username, password)[source]¶
Run the Haystack auth handshake and return a bearer auth token.
Attempts SCRAM-SHA-256 first, falling back to PLAINTEXT if offered.
- Parameters:
- Return type:
- Returns:
Bearer auth token string for subsequent requests.
- Raises:
AuthError – On authentication failure.
- hs_py.auth.scram_client_final(password, first, server_first_msg, hash_name='SHA-256')[source]¶
Build the SCRAM client-final message from the server-first response.
- Parameters:
password (
str) – The plaintext password.first (
ScramClientFirst) – TheScramClientFirstfromscram_client_first().server_first_msg (
str) – Raw server-first message (r=...,s=...,i=...).hash_name (
str(default:'SHA-256')) – Hash algorithm name (defaultSHA-256).
- Return type:
ScramClientFinal- Returns:
ScramClientFinalwith the proof message.- Raises:
AuthError – On nonce mismatch or unsupported hash.
Auth Types¶
Authentication protocol interfaces and credential backends.
Server-side authentication types.
Provides credential storage and authenticator protocols for Haystack server implementations. Framework-independent — used by both aiohttp and FastAPI servers.
- class hs_py.auth_types.Authenticator(*args, **kwargs)[source]¶
Bases:
ProtocolProtocol for server-side authentication.
- class hs_py.auth_types.CertAuthenticator(allowed_cns)[source]¶
Bases:
objectAuthenticator that authorizes clients by TLS client certificate CN.
When mutual TLS is enabled, the server can extract the Common Name (CN) from the client certificate and check it against an allowed set. This bypasses SCRAM entirely for certificate-authenticated connections.
- class hs_py.auth_types.ScramCredentials(salt, iterations, stored_key, server_key)[source]¶
Bases:
objectPre-computed SCRAM-SHA-256 credentials for a user.
- class hs_py.auth_types.SimpleAuthenticator(users, *, iterations=600000)[source]¶
Bases:
objectAuthenticator that derives SCRAM keys from a username→password dict.
- async scram_credentials(username)[source]¶
Return SCRAM credentials for a user, or
Noneif unknown.- Parameters:
username (
str) – The username to look up.- Return type:
- Returns:
ScramCredentialsorNone.
- class hs_py.auth_types.StorageAuthenticator(user_store)[source]¶
Bases:
objectAuthenticator that reads SCRAM credentials from a
UserStore.Returns
Nonefor disabled or missing users, preventing login.- Parameters:
user_store (
UserStore) – The user store to read credentials from.
- async scram_credentials(username)[source]¶
Return SCRAM credentials for a user, or
Noneif unknown/disabled.- Parameters:
username (
str) – The username to look up.- Return type:
- Returns:
ScramCredentialsorNone.
Users & Roles¶
User model, role-based permissions, and user creation helpers.
User model and helpers for Haystack server user management.
Provides a frozen User dataclass, a Role enum for
role-based access control, and a create_user() factory that derives
SCRAM-SHA-256 credentials from a plaintext password. Passwords are
never stored — only the derived ScramCredentials.
- class hs_py.user.Role(*values)[source]¶
Bases:
EnumUser role for permission enforcement.
Roles form a strict hierarchy: ADMIN > OPERATOR > VIEWER.
VIEWER — read-only Haystack ops (read, nav, hisRead, defs, …).
OPERATOR — read + write ops (hisWrite, pointWrite, invokeAction, watches).
ADMIN — full access including user management.
- VIEWER = 'viewer'¶
- OPERATOR = 'operator'¶
- ADMIN = 'admin'¶
- class hs_py.user.User(username, credentials, first_name='', last_name='', email='', role=Role.VIEWER, enabled=True, created_at=<factory>, updated_at=<factory>)[source]¶
Bases:
objectImmutable user record.
Passwords are stored as pre-computed
ScramCredentials— the plaintext is never retained.- Parameters:
username (
str) – Unique login identifier (required).credentials (
ScramCredentials) – SCRAM-SHA-256 credentials derived from the password.first_name (
str(default:'')) – User’s first name.last_name (
str(default:'')) – User’s last name.email (
str(default:'')) – User’s email address.role (
Role(default:<Role.VIEWER: 'viewer'>)) – Permission role (ADMIN, OPERATOR, or VIEWER).enabled (
bool(default:True)) – Whether the user can log in.created_at (
float(default:<factory>)) – Monotonic creation timestamp.updated_at (
float(default:<factory>)) – Monotonic last-update timestamp.
- credentials: ScramCredentials¶
- hs_py.user.create_user(username, password, *, first_name='', last_name='', email='', role=Role.VIEWER, enabled=True, iterations=600000)[source]¶
Create a
Userwith derived SCRAM credentials.- Parameters:
username (
str) – Unique login identifier.password (
str) – Plaintext password (used to derive credentials, then discarded).first_name (
str(default:'')) – User’s first name.last_name (
str(default:'')) – User’s last name.email (
str(default:'')) – User’s email address.role (
Role(default:<Role.VIEWER: 'viewer'>)) – Permission role (defaultRole.VIEWER).enabled (
bool(default:True)) – Whether the user can log in.iterations (
int(default:600000)) – PBKDF2 iteration count.
- Return type:
- Returns:
Frozen
Userinstance.
- hs_py.user.user_from_dict(d)[source]¶
Deserialize a
Userfrom a plain dict.Inverse of
user_to_dict().
Bootstrap¶
Admin user bootstrap on server startup.
Admin user bootstrap for Haystack servers.
On startup, verifies that at least one enabled admin user exists in the
UserStore. If none is found, attempts
to seed one from environment variables HS_SUPERUSER_USERNAME and
HS_SUPERUSER_PASSWORD. Exits with an error if neither source provides
an admin.
- async hs_py.bootstrap.ensure_superuser(store)[source]¶
Ensure at least one enabled admin user exists.
Check order:
Query the store for any enabled admin — if found, return.
Read
HS_SUPERUSER_USERNAMEandHS_SUPERUSER_PASSWORDfrom environment variables.If both are set, create a new admin user and persist it.
If either is missing, exit the process with an error message.
TLS¶
TLS configuration helpers for client and server SSL contexts, including mutual TLS (mTLS) and test certificate generation.
TLS 1.3 configuration and SSL context builders.
Provides TLSConfig for declaring certificate paths and helper functions
for constructing ssl.SSLContext instances that enforce TLS 1.3 minimum.
System CA trust is deliberately excluded — only explicitly configured CA certificates are loaded.
Uses the cryptography library for test certificate generation.
- class hs_py.tls.TLSConfig(certificate_path=None, private_key_path=None, ca_certificates_path=None, key_password=None)[source]¶
Bases:
objectTLS 1.3 configuration for Haystack client and server.
- Parameters:
certificate_path (
str|None(default:None)) – Path to PEM certificate file.private_key_path (
str|None(default:None)) – Path to PEM private key file.ca_certificates_path (
str|None(default:None)) – Colon-separated paths to CA PEM files or directories.key_password (
bytes|str|None(default:None)) – Passphrase for the private key (bytes or str).
- hs_py.tls.build_client_ssl_context(config)[source]¶
Build a TLS 1.3 client SSL context.
PROTOCOL_TLS_CLIENTenables hostname verification and requires server certificates by default.- Parameters:
config (
TLSConfig) – TLS configuration with certificate paths.- Return type:
- Returns:
Configured
ssl.SSLContext.
- hs_py.tls.build_server_ssl_context(config)[source]¶
Build a TLS 1.3 server SSL context with mutual authentication.
verify_mode = CERT_REQUIREDenforces client certificate verification.- Parameters:
config (
TLSConfig) – TLS configuration with certificate paths.- Return type:
- Returns:
Configured
ssl.SSLContext.
- hs_py.tls.extract_peer_cn(peercert)[source]¶
Extract the Common Name (CN) from a peer certificate dict.
- hs_py.tls.extract_peer_sans(peercert)[source]¶
Extract Subject Alternative Names from a peer certificate dict.
- hs_py.tls.generate_test_certificates(directory)[source]¶
Generate a self-signed CA and server/client certificates for testing.
Uses EC P-256 keys with SHA-256 signatures. Certificates are valid for one year from generation time. Writes PEM files to directory:
ca.pem— CA certificateserver.pem/server.key— Server certificate and private keyclient.pem/client.key— Client certificate and private key
FastAPI Server¶
FastAPI-based Haystack HTTP server with SCRAM authentication middleware, content negotiation, and WebSocket endpoint.
FastAPI-based Haystack HTTP server.
Provides a FastAPI application factory with content-negotiated Haystack routes, SCRAM-SHA-256 authentication middleware, and WebSocket support.
See: https://project-haystack.org/doc/docHaystack/HttpApi
- class hs_py.fastapi_server.ScramAuthMiddleware(app, authenticator, auth_tokens=None)[source]¶
Bases:
objectSCRAM-SHA-256 authentication middleware for FastAPI.
Pure ASGI middleware — avoids
BaseHTTPMiddlewareoverhead and streaming issues. Implements the Haystack SCRAM handshake:Client sends
Authorization: HELLO username=<b64>Server returns 401 with
WWW-Authenticate: SCRAM handshakeToken=...Client sends
Authorization: SCRAM handshakeToken=..., data=<client-first>Server returns 401 with server-first message
Client sends
Authorization: SCRAM handshakeToken=..., data=<client-final>Server returns 200 with
Authentication-Info: authToken=..., data=<server-final>Subsequent requests use
Authorization: BEARER authToken=...
- Parameters:
app (
Callable[[MutableMapping[str,Any],Callable[[],Awaitable[MutableMapping[str,Any]]],Callable[[MutableMapping[str,Any]],Awaitable[None]]],Awaitable[None]]) – The ASGI application to wrap.authenticator (
Authenticator) – Server-side credential store.auth_tokens (
dict[str,TokenEntry] |None(default:None)) – Shared token dict (also used by the WS endpoint).
- hs_py.fastapi_server.create_fastapi_app(ops=None, *, storage=None, authenticator=None, namespace=None, user_store=None, prefix='/api', cors_origins=None, default_format='json')[source]¶
Create a FastAPI application with Haystack HTTP routes.
The returned app supports content-negotiated responses (JSON, Zinc, Trio, CSV, Turtle, JSON-LD), SCRAM-SHA-256 authentication (when an authenticator is provided), and WebSocket connections at
{prefix}/ws.- Parameters:
ops (
HaystackOps|None(default:None)) –HaystackOpsimplementation to dispatch to. When None and storage is provided, a defaultHaystackOpsis constructed automatically.storage (
StorageAdapter|None(default:None)) – OptionalStorageAdapterbackend. Itsstart()/close()lifecycle methods are called automatically via the FastAPI lifespan.authenticator (
Authenticator|None(default:None)) – OptionalAuthenticatorfor SCRAM-SHA-256 auth. When None, all requests are accepted without auth.namespace (
Namespace|None(default:None)) – OptionalNamespacefordefsandlibsoperations.user_store (
UserStore|None(default:None)) – OptionalUserStorefor user management. When provided, user CRUD endpoints are mounted under{prefix}/users/and superuser bootstrapping runs at startup.prefix (
str(default:'/api')) – URL path prefix for all Haystack routes (default"/api").cors_origins (
list[str] |None(default:None)) – Optional list of allowed CORS origins. When provided,CORSMiddlewareis added with credentials support. Example:["http://localhost:3000", "https://app.example.com"].default_format (
str(default:'json')) – Default response format when the client sends*/*or noAcceptheader. The Haystack spec defines"zinc"as the default; this library defaults to"json"for ecosystem compatibility. Supported values:"json","zinc".
- Return type:
FastAPI- Returns:
Configured
fastapi.FastAPIapplication.
Example:
from hs_py.fastapi_server import create_fastapi_app from hs_py.storage.memory import InMemoryAdapter from hs_py.auth_types import StorageAuthenticator storage = InMemoryAdapter() auth = StorageAuthenticator(storage) app = create_fastapi_app(storage=storage, authenticator=auth, user_store=storage) # uvicorn.run(app, host="0.0.0.0", port=8080)