Customization¶
Keysmith is designed to be extended without forking. Swap models, hashers, and hooks through settings.
Swappable models¶
Subclass the abstract bases:
from keysmith.models.base import AbstractToken, AbstractTokenAuditLog
class MyToken(AbstractToken):
organization = models.ForeignKey("orgs.Organization", on_delete=models.CASCADE)
class Meta(AbstractToken.Meta):
db_table = "my_token"
class MyAuditLog(AbstractTokenAuditLog):
class Meta(AbstractTokenAuditLog.Meta):
db_table = "my_audit_log"
Django system checks (keysmith.E001–E003) validate required fields and methods at startup. See Models reference.
Use get_token_model() and get_audit_log_model() in your code instead of importing concrete models directly.
Custom hash backend¶
Implement keysmith.hashers.base.BaseTokenHasher:
from keysmith.hashers.base import BaseTokenHasher
class FastTokenHasher(BaseTokenHasher):
def hash(self, secret: str) -> str:
...
def verify(self, secret: str, hashed: str) -> bool:
...
The default PBKDF2SHA512TokenHasher uses Django's PBKDF2PasswordHasher with algorithm="pbkdf2_sha512" and iterations=HASH_ITERATIONS.
Hooks¶
Hooks are loaded via import_string from dotted paths in settings.
Rate limit (middleware)¶
DRF throttle¶
from rest_framework.exceptions import Throttled
def drf_throttle(request, token=None):
if should_throttle(token):
raise Throttled()
Audit sink¶
def audit(*, action, token, request, status_code, extra, payload):
"""Replaces default DB write entirely."""
publish(action, payload)
payload contains: path, method, status_code, ip_address, user_agent.
Error messages¶
Override user-facing strings without changing code:
KEYSMITH = {
"DEFAULT_ERROR_MESSAGES": {
"invalid_token": "Invalid API key.",
"insufficient_scope": "Insufficient permissions.",
},
}
Unspecified keys keep their built-in defaults (lazy-translated).
Settings proxy¶
from keysmith.settings import keysmith_settings
keysmith_settings.HASH_ITERATIONS
keysmith_settings.reload() # clear cached values (tests)