omnixys-security
Shared security toolkit for Omnixys services: JWT validation against a JWKS endpoint, pluggable ASGI middleware, per-request context, rate limiting, and timing-safe Argon2id password hashing.
Installation
pip install omnixys-security
Features
- JwtValidator — validates RS256 JWTs against a remote JWKS with in-memory caching, issuer and audience checks.
- SecurityMiddleware — ASGI middleware that resolves the request context from a bearer token or internal API key.
- RequestContext — typed, context-local request metadata (user, roles, tenant, request/correlation id, client IP).
- RateLimiter / RateLimitMiddleware — fixed-window Redis-backed rate limiting by IP, user, and tenant.
- HashService — Argon2id password hashing with optional pepper and timing-safe
dummy_verify. - SecurityError hierarchy — typed exceptions with stable codes for authz failures, expired tokens, tenant errors.
Quick start
from security import JwtValidator, SecurityMiddleware, RateLimitMiddleware, RateLimiter
jwt_validator = JwtValidator(
jwks_url="https://auth.omnixys.com/.well-known/jwks.json",
issuer="https://auth.omnixys.com/realms/omnixys",
audience="omnixys-api",
)
redis = redis.asyncio.from_url("redis://localhost:6379")
limiter = RateLimiter(redis, default_limit=120, default_window_ms=60000)
app = SecurityMiddleware(
app=my_asgi_app,
jwt_validator=jwt_validator,
internal_api_key=os.getenv("INTERNAL_API_KEY"),
)
app = RateLimitMiddleware(app=app, limiter=limiter)
Request context
Within a handled request you can read the resolved identity at any point:
from security import current_request_context
async def handler():
ctx = current_request_context()
if ctx.user_id and "admin" in ctx.roles:
await do_admin_thing(ctx.tenant_id)
else:
raise AccessDeniedError()
RequestContext carries user_id, username, email, first_name,
last_name, roles, scopes, tenant_ids, tenant_id (pinned via the
x-tenant-id header when it is contained in the token), correlation_id,
request_id, client_ip, is_authenticated, and is_internal.
The middleware always resets the context after the request, so stale identity never leaks between requests.
JWT validation
from security import JwtValidator
validator = JwtValidator(
jwks_url="...",
issuer="...",
audience="...",
cache_ttl_seconds=900,
)
claims = await validator.validate(token)
assert claims.user_id == claims.sub
assert claims.roles == ["admin"]
validate raises ValueError when the token is invalid, expired, from a
different issuer, or signed by an unknown key. JWKS responses are cached for
cache_ttl_seconds to avoid a fetch per request.
Rate limiting
from security import RateLimiter, RateLimitMiddleware
limiter = RateLimiter(redis, default_limit=120, default_window_ms=60000)
async def view():
if not await limiter.is_allowed(await limiter.user_key(ctx.user_id)):
return json_response({"error": "rate_limit_exceeded"}, status=429)
RateLimitMiddleware checks IP, then user, then tenant (in that order) and
responds 429 {"error":"rate_limit_exceeded"} when a limit is hit. Paths such
as /health and /metrics are excluded by default and never limited.
Password hashing
from security import HashOptions, HashService
service = HashService(HashOptions(pepper=os.getenv("PEPPER", "")))
stored = service.hash(password) # $argon2id$...
ok = service.verify(stored, password) # never raises
needs_upgrade = service.needs_rehash(stored)
# call for unknown users to equalize timing with known-user lookups
service.dummy_verify()
Errors
All domain errors subclass SecurityError and carry a stable code:
from security import AccessDeniedError, InvalidCredentialsError
try:
authorize(ctx, "events.write")
except AccessDeniedError as exc:
print(exc.code, exc.to_dict())
The hierarchy includes TokenInvalidError, InvalidCredentialsError,
RefreshTokenExpiredError, AccessDeniedError, EventAccessDeniedError,
TenantNotFoundError, TenantDisabledError,
TenantMembershipNotFoundError, and TenantMembershipInactiveError.
Development
uv sync
uv run pytest -q
uv run ruff check .
uv run mypy src/
License
GPL-3.0-or-later
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file omnixys_security-4.0.0.tar.gz.
File metadata
- Download URL: omnixys_security-4.0.0.tar.gz
- Upload date:
- Size: 53.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
703bca6008e571e74f76a79c8e3785e15aa05576ad56f426e8fb4080fa100676
|
|
| MD5 |
65e2316885ed870173e13cdc9eb751c9
|
|
| BLAKE2b-256 |
4f74b601488ed927f3d05dd69835ce6911ff1056c8d47fdd1af1f0fd6f1a13fe
|
File details
Details for the file omnixys_security-4.0.0-py3-none-any.whl.
File metadata
- Download URL: omnixys_security-4.0.0-py3-none-any.whl
- Upload date:
- Size: 9.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a4d92340eb5042416859b2bd62305433b756903c312d1801bb7a50e1f9a2069
|
|
| MD5 |
f65db2c16b30d400ffa18ff0fc67861b
|
|
| BLAKE2b-256 |
ce26e95592756a9a6689e93066ff5c0434a49df907146f24dcd6ced6b52aa058
|