django-tenant-apikeys
Multi-tenant API key authentication for Django
Issue a key per tenant, hash it before it ever touches the database, gate access with scopes instead of an all-or-nothing flag, restrict it by IP, rate-limit it, and rotate or revoke it later without touching your own code. First-class Django REST Framework and Django Ninja support.
If you've built API key auth for a SaaS product before, you've probably
written this same code three or four times: generate a random token, hash
it, store the hash, look it up on every request, and figure out how to show
the raw key to the user exactly once — then bolt on a tenant relation, a
scopes field, and an IP allowlist by hand. django-tenant-apikeys is that
code, written once and tested against every rejection path, not just the
happy one.
- Bring your own tenant model. Subclass one abstract model, add a
tenantforeign key, andrequest.tenantis resolved automatically on every authenticated request. - Nothing sensitive is stored. Keys are
secrets-generated, kept only as a SHA-256 hash, checked with a constant-time comparison, and shown to you exactly once. - Scopes, not just on/off.
"orders:read", a whole namespace with"orders:*", or everything with"*". - Per-key IP allowlists and rate limits. IPv4/IPv6, CIDR networks, and a configurable per-key request limit — optional per key. The allowlist (and, if you set it, the environment) is enforced during authentication, identically in DRF and Ninja, so a view can't forget it; the rate limit is a native DRF/Ninja throttle.
- A real lifecycle.
rotate()issues a replacement — revoking the original at once, or letting it run out over a grace period, withrotated_fromrecording which key replaced which;revoke()/reactivate()manage access directly; signals and a logger tell you what happened. - Small, framework-agnostic core. The model doesn't know DRF or Ninja exist. Everything framework-specific is an optional adapter on top.
Install
pip install django-tenant-apikeys[drf]
# myapp/models.py
class OrganizationAPIKey(AbstractTenantAPIKey):
tenant = models.ForeignKey(Organization, related_name="api_keys", on_delete=models.CASCADE)
instance, raw_key = OrganizationAPIKey.generate_key(
name="CI deploy key",
tenant=org,
scopes=["deployments:write"],
)
print(raw_key) # tak_live_3f9a2c1d.k7pQ2m1v... -- shown once, never stored
# myapp/views.py
class DeploymentsView(APIView):
authentication_classes = [TenantAPIKeyAuthentication]
permission_classes = [HasAPIKeyScope]
required_scopes = ["deployments:write"]
def post(self, request):
request.tenant.deployments.create(...) # attached automatically
Full walkthrough: Quickstart.
A complete runnable project lives in
examples/simple_saas/ — clone the repo, run
python manage.py create_demo_key, and you have a real tenant-scoped key
and a working curl command in about a minute.
Documentation
Full documentation, including configuration, IP restrictions, rate limiting, the Django Ninja integration, and the complete API reference, is at stackadnan.github.io/django-tenant-apikeys.
| Installation · Quickstart · Configuration | Getting started |
| Authentication · Multi-tenancy · Scopes · Key lifecycle | Core concepts |
| Environments · IP restrictions · Rate limiting · Metadata | Per-key policies |
| Django REST Framework · Django Ninja | Framework integrations |
| Admin · Management commands · Security · Testing · Troubleshooting | Operating it |
| API reference | Reference |
The wiki has a
few longer standalone articles — a factual comparison against
djangorestframework-api-key, shared-schema vs. schema-per-tenant
multi-tenancy, and a general API-key security checklist — that go deeper
than the reference docs on those specific topics.
Supported versions
- Python 3.10, 3.11, 3.12, 3.13, 3.14
- Django 4.2, 5.2 and 6.1, on the Python versions each supports (tested in CI; other 4.x/5.x/6.x releases are likely to work but aren't part of the test matrix)
- Django REST Framework ≥ 3.14 and/or django-ninja ≥ 1.2, both optional
Development
git clone https://github.com/stackadnan/django-tenant-apikeys
cd django-tenant-apikeys
pip install -e ".[dev]"
pytest --cov=django_tenant_apikeys --cov-report=term-missing # 100% coverage enforced
ruff check .
mypy django_tenant_apikeys
Those three commands are exactly what CI runs on every push and PR — see CONTRIBUTING.md for the full workflow, and Testing for running against PostgreSQL and testing your own authentication code.
License
Metadata
Release files for django-tenant-apikeys 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_tenant_apikeys-0.6.0.tar.gz | 66.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_tenant_apikeys-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 104.1 kB
Release files / django_tenant_apikeys-0.6.0.tar.gz
| Download URL | django_tenant_apikeys-0.6.0.tar.gz |
|---|---|
| Size | 66.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
53dca80aa59d8290530c0e6b195b7446744937983e50ec2e2190bd3d9318c9f3
|
|
BLAKE2b-256 checksum How to use checksums |
f8608496887743a3e57a8ddbc0a17d8524f47858f92edd9990a7c2b554556c7c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / django_tenant_apikeys-0.6.0-py3-none-any.whl
| Download URL | django_tenant_apikeys-0.6.0-py3-none-any.whl |
|---|---|
| Size | 37.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ca05c2a2884b232183c0a1ea2dfb5a26b485f6b01b752e8da94b405e282af42c
|
|
BLAKE2b-256 checksum How to use checksums |
2bac6fe0effe7f79a030a7b475b9c86922c5535577d1c3933c0f1b9d176d1140
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency log