JWT issuance/validation, API key hashing, bcrypt passwords, and scoped principals for AI-native platforms.
Standalone auth primitives — no FastAPI required. All core functions work with any Python web framework or no framework at all.
Status: v0.1.0 — prepared, not yet published.
pip install nodus-authbcrypt version note: This package pins
bcrypt>=4.0.1,<5.0becausepasslib 1.7.4is incompatible with bcrypt 5.x. Do not upgrade bcrypt beyond 4.x in the same environment.
| Component | Purpose |
|---|---|
KeyRing / create_access_token / decode_access_token |
JWT issuance and verification with key rotation |
hash_password / verify_password |
bcrypt password hashing via passlib |
generate_key / hash_key |
API key generation and SHA-256 storage hash |
AuthPrincipal / Scopes |
Resolved identity with scope-based access control |
LoginRequest / RegisterRequest / TokenResponse |
Pydantic request/response schemas |
parse_user_id / require_user_id |
UUID parsing helpers |
AuthSettings |
pydantic-settings config for SECRET_KEY, ALGORITHM, expiry |
from nodus_auth import AuthSettings, create_access_token, decode_access_token, InvalidTokenError
settings = AuthSettings(SECRET_KEY="my-secret-key-32-chars-minimum")
token = create_access_token({"sub": "user-123"}, settings=settings)
try:
payload = decode_access_token(token, settings=settings)
user_id = payload["sub"]
except InvalidTokenError:
# expired, malformed, or wrong key
...from nodus_auth import KeyRing, create_access_token, decode_access_token, AuthSettings
ring = KeyRing(active="key-v1", grace_hours=24)
settings = AuthSettings(SECRET_KEY="key-v1")
token = create_access_token({"sub": "u1"}, settings=settings, key_ring=ring)
ring.rotate("key-v2") # tokens signed with key-v1 still verify for 24 hours
payload = decode_access_token(token, settings=settings, key_ring=ring) # OKdecode_access_token tries the active key first, then any keys within their
grace period. Tokens outside their grace window raise InvalidTokenError.
from nodus_auth import hash_password, verify_password
hashed = hash_password("correct-horse-battery-staple")
assert verify_password("correct-horse-battery-staple", hashed)
assert not verify_password("wrong-password", hashed)from nodus_auth import generate_key, hash_key
raw_key, key_hash = generate_key()
# Deliver raw_key to the caller once; store key_hash in the database
assert hash_key(raw_key) == key_hashgenerate_key() uses secrets.token_urlsafe. The raw key is never stored;
the SHA-256 hash is used for all comparisons.
from nodus_auth import AuthPrincipal, Scopes
principal = AuthPrincipal(
user_id="user-123",
auth_type="api_key",
scopes=[Scopes.MEMORY_READ, Scopes.FLOW_EXECUTE],
)
assert principal.has_scope(Scopes.FLOW_EXECUTE)
assert not principal.has_scope(Scopes.PLATFORM_ADMIN)auth_type is "jwt" or "api_key". Scopes provides well-known constants:
MEMORY_READ, MEMORY_WRITE, FLOW_EXECUTE, FLOW_CREATE, PLATFORM_ADMIN.
from nodus_auth import AuthSettings
# Reads from environment variables: SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
settings = AuthSettings()
# Or explicit:
settings = AuthSettings(
SECRET_KEY="...",
ALGORITHM="HS256",
ACCESS_TOKEN_EXPIRE_MINUTES=60,
)from nodus_auth import parse_user_id, require_user_id, parse_user_ids
import uuid
parse_user_id("550e8400-e29b-41d4-a716-446655440000") # → UUID
parse_user_id(None) # → None
require_user_id("not-a-uuid") # raises ValueError
parse_user_ids(["uid-1", "bad", "uid-2"]) # → [UUID, UUID] (skips failures)| Package | Version | Purpose |
|---|---|---|
python-jose |
≥3.5.0 | JWT encoding/decoding |
passlib |
≥1.7.4 | bcrypt password context |
bcrypt |
≥4.0.1,<5.0 | bcrypt backend (passlib 1.7.4 incompatible with 5.x) |
pydantic |
≥2.0.0 | Schemas |
pydantic-settings |
≥2.0.0 | AuthSettings from env vars |
pip install -e ".[dev]"
pytest tests/ -qMIT — see LICENSE.