Skip to content

Scoped, hashed API keys in sillo: APIKeyAuthBackend, ApiKey/ApiKeyManager for storage, and ApiKeyUserMixin for issuing keys from a user.

API keys are for server-to-server and programmatic access: a long-lived secret sent in a header, scoped to specific permissions, and stored hashed (never plaintext). sillo gives you the full lifecycle: generate, verify, list, revoke.

from sillo.auth import AuthenticationMiddleware, useAuth
from sillo.auth.apikey import APIKeyAuthBackend
from sillo.users import User
app.use(AuthenticationMiddleware(
user_model=User,
backend=APIKeyAuthBackend(header_name="X-API-Key", verify_with_manager=True),
))
@app.get("/v1/data", auth=useAuth(schemes=["apiKeyHeader"]))
async def data(request, response):
return {"called_by": request.user.identity}

Backend parameters:

ParamDefaultMeaning
header_name"X-API-Key"Header to read the key from.
prefix"key"Expected key prefix (key is prefix_xxxx).
verify_with_managerFalseWhen True, validates the key against an ApiKey DB row (checks hash, expiry, is_active).

On success, request.scope["auth"] becomes "apikey".

Keys are hashed with SHA-256 before storage. You never persist the raw key.

from sillo.auth.apikey import ApiKey, ApiKeyManager
# Generate: returns (full_key, raw, hash)
full_key, raw, key_hash = await ApiKey.generate_api_key(prefix="sillo")
# Persist a row for a user
await ApiKey.create(
user_id=1,
name="ci-pipeline",
key_hash=key_hash,
scopes=["read:data", "write:data"],
expires_at=None, # or datetime in the future
)
# Verify a presented key against the stored hash
ok = ApiKey.verify_api_key(raw, key_hash)
# Look up / revoke via the manager
keys = await ApiKeyManager().get_for_user(1)
await ApiKeyManager().revoke_all_for_user(1)

ApiKey fields: name, key_hash (unique), user_id, scopes (JSON list), last_used_at, expires_at, is_active. Helpers: mark_used(), revoke(), and the property is_expired.

3. Issuing keys from a user: ApiKeyUserMixin

Section titled “3. Issuing keys from a user: ApiKeyUserMixin”

Add ApiKeyUserMixin to your user class so a user can self-service keys:

from sillo.users import User
from sillo.auth.apikey.mixins import ApiKeyUserMixin
class AppUser(User, ApiKeyUserMixin):
...
user = await AppUser.load_user("1")
full_key, apikey = await user.create_api_key(
name="ci",
scopes=["read:data"],
expires_at=None,
prefix="sillo",
)
# full_key is the only time you see the raw secret; apikey is the DB row
keys = await user.get_api_keys() # list active key rows
await user.revoke_api_key(key_id) # revoke one
await user.revoke_all_api_keys() # revoke all for this user

create_api_key returns (full_key, ApiKey). full_key is shown once; afterwards only the hash exists in the database.

The backend confirms that the key is valid but does not expose the key’s scopes on the request, so scope enforcement is your job. Re-verify the presented key in the handler to get the ApiKey row (which carries scopes):

from sillo.auth.apikey import ApiKeyManager
@app.get("/v1/data", auth=useAuth(schemes=["apiKeyHeader"]))
async def data(request, response):
raw = request.headers.get("X-API-Key")
apikey = await ApiKeyManager().verify(raw) # returns the ApiKey row or None
if apikey is None or "read:data" not in (apikey.scopes or []):
return response.json({"error": "insufficient scope"}, status_code=403)
...

API keys commonly sit alongside JWT/session so the same endpoints serve both humans and machines:

app.use(AuthenticationMiddleware(
user_model=User,
backend=[
APIKeyAuthBackend(header_name="X-API-Key", verify_with_manager=True),
JWTAuthBackend(secret_key=JWT_SECRET, identifier="sub"),
SessionAuthBackend(),
],
))

A request with X-API-Key authenticates as "apiKeyHeader"; one with a bearer token as "bearerAuth"; one with a cookie as "sessionCookie". Routes can pin a credential with useAuth(schemes=["apiKeyHeader"]) or accept any with useAuth().