skip to content

Auth & Throttling

auth= on the API, a router or one operation takes APIKeyHeader, HttpBearer or django_auth, which checks CSRF, and throttle= adds rate classes. Interviewers probe where auth is inherited.

on this pageshow

explore

questions

5

In Django Ninja, how do you protect a partner operation with an APIKeyHeader authenticator, and what does request.auth hold afterwards?

level: juniorimportance: must knowfreq 50%

answer

  1. subclass, then one method
  2. which header name, by default
  3. truthy return value is kept
  4. falsy everywhere means 401

basics

~10 s

Subclass 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 s

I 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 lines
python
import 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

for a junior

Recall the pattern: subclass APIKeyHeader, set param_name, implement authenticate(), pass an instance to auth=, read request.auth in the view.

for a middle

Explain the chain: list order, truthy wins, falsy moves on, raising stops everything, and 401 when all decline.

for a senior

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.

for a principal

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.
open as a page

In Django Ninja, with auth= set on the NinjaAPI, a Router and an operation, which applies, and how is one operation made public?

level: middleimportance: must knowfreq 45%

basics

~10 s

The most specific auth= replaces the others: operation, then the add_router() mount, then Router(auth=), then a parent router, then NinjaAPI(auth=). Settings are never merged. auth=None on an operation or router makes it public.

open as a page

In Django Ninja, how do AnonRateThrottle, AuthRateThrottle and UserRateThrottle differ in which requests they count and how they key them?

level: middleimportance: should knowfreq 30%

basics

~20 s

AnonRateThrottle counts only requests without request.auth, keyed by client IP; AuthRateThrottle keys by a hash of str(request.auth); UserRateThrottle keys by the Django request.user's primary key. The last two fall back to IP for anonymous callers.

open as a page

Why does a Django Ninja POST guarded by auth=[django_auth, PartnerBearer()] return 403 to a partner sending a valid bearer token, and how do you fix it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

django_auth is cookie-based, so on unsafe methods it runs Django's CSRF check first and raises HttpError(403) when it fails; the raise ends the chain before PartnerBearer runs. Put PartnerBearer first or give partners a separate router.

open as a page

A Django Ninja partner endpoint uses AuthRateThrottle('600/m'), yet in production partners share one limit or exceed it freely - what do you check?

level: seniorimportance: nice to knowfreq 20%

basics

~10 s

Check that str(request.auth) is unique per partner, since AuthRateThrottle hashes it; that CACHES is a shared backend rather than per-process LocMemCache; and, for IP fallbacks, NINJA_NUM_PROXIES. Throttles also share keys across operations.

open as a page