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(scopes=["apikey"]))
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".

2. Storing keys — ApiKey & ApiKeyManager

Section titled “2. Storing keys — ApiKey & ApiKeyManager”

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(scopes=["apikey"]))
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 "apikey"; one with a bearer token as "jwt"; one with a cookie as "session". Routes can pin a method with useAuth(scopes=["apikey"]) or accept any with useAuth().