In Django Ninja, how do you protect a partner operation with an APIKeyHeader authenticator, and what does request.auth hold afterwards?
answer
- subclass, then one method
- which header name, by default
- truthy return value is kept
- falsy everywhere means 401
basics
~10 sSubclass ninja.security.APIKeyHeader, set param_name to the header, and implement authenticate(request, key) returning the partner or None; pass an instance as auth=. Ninja stores the truthy return value in request.auth, or answers 401.
solid answer
~40 sI subclass `ninja.security.APIKeyHeader`, set `param_name = "X-API-Key"` - the default is a header literally called `key` - and implement `authenticate(self, request, key)`, which looks the key up and returns the matching `Partner` or `None`. An instance goes on `auth=` of the operation, a `Router` or the `NinjaAPI`. Before the view runs, Ninja calls the authenticator; any **truthy** return value is stored in `request.auth`, so the view reads `request.auth` to know which partner is calling. A falsy result makes Ninja try the next authenticator in the list, and if none succeeds it answers `401` with `{"detail": "Unauthorized"}` without calling the view. `APIKeyQuery` and `APIKeyCookie` work the same way from the query string and a cookie, and `HttpBearer` passes the token from `Authorization: Bearer ...`.
code
python · 38 linesimport hashlib
from django.db import models
from ninja import NinjaAPI, Schema
from ninja.security import APIKeyHeader
class Partner(models.Model):
name = models.CharField(max_length=100)
key_digest = models.CharField(max_length=64, unique=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return f"partner:{self.pk}"
class PartnerKey(APIKeyHeader):
param_name = "X-API-Key"
def authenticate(self, request, key):
if not key:
return None
digest = hashlib.sha256(key.encode()).hexdigest()
return Partner.objects.filter(key_digest=digest, is_active=True).first()
api = NinjaAPI()
class OrderIn(Schema):
sku: str
quantity: int
@api.post("/partner/orders", auth=PartnerKey())
def submit_order(request, payload: OrderIn):
partner = request.auth # the Partner returned by authenticate()
return {"partner": partner.name, "sku": payload.sku}go deeper
Recall the pattern: subclass APIKeyHeader, set param_name, implement authenticate(), pass an instance to auth=, read request.auth in the view.
Explain the chain: list order, truthy wins, falsy moves on, raising stops everything, and 401 when all decline.
Return an identity object rather than True, keep keys out of query strings, and choose header, bearer or cookie authenticators knowing which ones trigger CSRF checks.
Decide how partner identity flows from the authenticator into authorisation and rate limiting, so request.auth is a stable, documented contract across the API.
## What auth= expects In **Django Ninja**, an operation is protected by passing an **authenticator** to `auth=`. An authenticator is any callable that takes the Django `request` and returns something: a **truthy** value means the caller is authenticated, and Ninja stores that value in **`request.auth`**; a falsy value (`None`, `False`, an empty string) means "not me". The built-in classes in `ninja.security` are ready-made callables that extract a credential from the request and hand it to a method you write. | class | where it reads the credential | method you implement | |---|---|---| | `APIKeyHeader` | `request.headers[param_name]` | `authenticate(request, key)` | | `APIKeyQuery` | `request.GET[param_name]` | `authenticate(request, key)` | | `APIKeyCookie` | `request.COOKIES[param_name]`, with a CSRF check | `authenticate(request, key)` | | `HttpBearer` | `Authorization: Bearer <token>` | `authenticate(request, token)` | | `HttpBasicAuth` | `Authorization: Basic <base64>` | `authenticate(request, username, password)` | | `django_auth` | the Django session (`request.user`) | nothing - it is a ready instance | For the three API-key classes, **`param_name` defaults to `"key"`**. An `APIKeyHeader` subclass that forgets to set it reads a header named `key`, so every partner sending `X-API-Key` gets a 401. ## Building a partner authenticator The steps for a partner endpoint are: 1. Subclass `APIKeyHeader` and set `param_name = "X-API-Key"`. 2. Implement `authenticate(self, request, key)`. `key` is `None` when the header is absent. 3. Look the key up and return the **object that identifies the caller** - a `Partner` instance - or `None`. 4. Pass an **instance** to `auth=`: `@api.post("/partner/orders", auth=PartnerKey())`. 5. In the view, read `request.auth` to get the partner. How the key is generated, hashed at rest and rotated is key-management design, not Ninja API; the authenticator only has to turn a header value into a caller or `None`. ## What happens on each request Before parameter validation and before the view, Ninja runs the operation's authenticators **in list order**: - the first one that returns a truthy value wins; its value goes into `request.auth` and the rest are skipped; - a falsy value moves on to the next authenticator; - an authenticator that **raises** - for example `raise HttpError(403, "Partner suspended")` - ends the chain immediately, and the exception's handler produces the response; - if every authenticator returns something falsy, Ninja raises `ninja.errors.AuthenticationError`, rendered as **401** `{"detail": "Unauthorized"}`. So `auth=[PartnerKey(), django_auth]` accepts either a partner key or a logged-in Django user, and the view must cope with `request.auth` being either a `Partner` or a `User`. ## Returning something useful `request.auth` is exactly what `authenticate()` returned. Returning `True` passes the check but leaves the view no way to know who called, and it breaks anything keyed on the caller, such as per-caller throttling that uses `str(request.auth)`. Returning the `Partner` gives the view the identity and lets authorisation checks read its fields, for example which partner owns the order being updated. ## Header, query or bearer - **`APIKeyHeader`** suits server-to-server partners: the key stays out of URLs. - **`APIKeyQuery`** puts the key in the URL, where it tends to end up in access logs and browser history; keep it for cases where headers cannot be set. - **`HttpBearer`** is the choice when the credential is a bearer token; it strips the `Bearer` scheme (case-insensitively) and returns `None` for a missing header or a different scheme. - **`APIKeyCookie`** and `django_auth` read cookies, which browsers attach automatically, so Ninja runs a CSRF check for them on unsafe methods. ## Testing an authenticator `ninja.testing.TestClient` calls operations directly and makes the behaviour easy to pin down. Three cases cover most of it: - a request without the header returns 401 and never reaches the view; - a request with an unknown or inactive key returns 401; - a request with a valid key returns 200, and the view sees the right partner in `request.auth`. The client accepts `headers={"X-API-Key": "..."}` on each call. A fourth test that sends the key under the wrong header name catches a missing `param_name` before a partner does, and a test that returns an inactive partner proves the lookup filters on status rather than only on the key. ## Mistakes interviewers probe - Passing the class instead of an instance: `auth=PartnerKey` makes Ninja call the class with the request. - Forgetting `param_name` and debugging 401s for a header that is never read. - Returning a queryset from `authenticate()`: an unevaluated queryset is truthy even when empty, so a wrong key passes. - Expecting `request.user` to be the partner: `request.user` comes from Django's session middleware, while Ninja puts the authenticator's result in `request.auth`.
- How do you reject a known but suspended partner with 403 instead of 401 in Django Ninja?Raise from `authenticate()`: `raise HttpError(403, "Partner suspended")` from `ninja.errors`, or `AuthorizationError()`, which defaults to 403. An exception ends the authenticator chain and goes to the API's exception handlers, while returning `None` would only produce the generic 401 once every authenticator declined.
- What does HttpBearer do with a header like Authorization: Token abc?It returns `None` without calling `authenticate()`, because the scheme is compared case-insensitively against `bearer`. With `DEBUG` on it also logs the unexpected value. The request then falls through to the next authenticator or ends in a 401.
saying these in an interview costs you the question
- APIKeyHeader reads the X-API-Key header unless told otherwise.
- request.user is set to whatever authenticate() returns.
- Returning True from authenticate() is as good as returning the partner.
- When authentication fails, the view runs with request.auth set to None.
- All authenticators in the list must pass for the request to proceed.