Skip to content

Browser and mobile clients

Which credential a client uses decides most of the setup:

Client Credential Ninja auth=
Single-page app on the same site as the API (app.example.com → api.example.com) Django session cookie + CSRF token django_auth
Mobile or desktop app Token in a header, issued by django-allauth's headless API x_session_token_auth or jwt_token_auth
Scripts, CI jobs, other servers Scoped API key APIKeyAuth()

Several can be accepted at once: NinjaAPI(auth=[django_auth, APIKeyAuth()]).

Single-page apps: sessions and CSRF

Sessions keep the credential in an HttpOnly cookie that scripts cannot read, and Django's login, logout, password validation and session expiry apply unchanged. Ninja checks the CSRF token for every cookie-authenticated request whose method is not GET, HEAD, OPTIONS or TRACE.

from django.contrib.auth import authenticate, login, logout
from django.middleware.csrf import get_token
from django.views.decorators.csrf import csrf_protect
from ninja import Schema, Status
from ninja.decorators import decorate_view
from ninja.security import django_auth

from ninja_devx import Controller, get, post


class Credentials(Schema):
    username: str
    password: str


class SessionController(Controller):
    @get("/csrf", auth=None)
    def csrf(self, request) -> dict[str, str]:
        return {"csrf_token": get_token(request)}

    @post("/login", auth=None, response={204: None, 401: dict})
    @decorate_view(csrf_protect)  # no auth means no automatic CSRF check
    def login(self, request, credentials: Credentials):
        user = authenticate(request, **credentials.model_dump())
        if user is None:
            return Status(401, {"detail": "Invalid credentials."})
        login(request, user)
        return Status(204, None)

    @post("/logout", auth=django_auth, response={204: None})
    def logout(self, request):
        logout(request)
        return Status(204, None)

The client fetches the token once, then sends it back as X-CSRFToken with every unsafe request, and with cookies included:

const { csrf_token } = await (await fetch(`${API}/session/csrf`, { credentials: "include" })).json();

await fetch(`${API}/session/login`, {
  method: "POST",
  credentials: "include",
  headers: { "Content-Type": "application/json", "X-CSRFToken": csrf_token },
  body: JSON.stringify({ username, password }),
});

The token comes back in the response body instead of being read from the csrftoken cookie because a page on app.example.com cannot read a cookie set by api.example.com. Fetch it again after login: Django rotates the token when the user changes.

When the app and the API are on different origins of the same site, allow the app's origin explicitly (django-cors-headers for the CORS part):

CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_CREDENTIALS = True
SESSION_COOKIE_SECURE = CSRF_COOKIE_SECURE = True
# SameSite=Lax (the default) is enough within one site. An app on another site entirely
# would need SameSite=None, which browsers increasingly block: use tokens there instead.

QUERY operations are safe by the HTTP specification, but Django's CSRF middleware does not know the method yet and treats it as unsafe, so send X-CSRFToken with them too.

Mobile apps: django-allauth headless tokens

Native apps do not share a cookie jar with a browser and have no CSRF exposure, so they send a token header. django-allauth's headless API provides the login, signup, email verification, MFA and social login endpoints, and security classes for Ninja:

from allauth.headless.contrib.ninja.security import jwt_token_auth, x_session_token_auth

api = NinjaAPI(auth=[x_session_token_auth, django_auth])  # app tokens or browser sessions

x_session_token_auth reads the X-Session-Token header that allauth's app client receives after login. jwt_token_auth accepts access tokens when allauth is configured with its JWT token strategy. ninja-devx permissions (IsAuthenticated, IsOwner, object permissions) take the user from request.auth when an auth class returns a user, else from request.user, so they apply the same way as with sessions.

Prefer these over writing a token system: issuing, refreshing, revoking and rate limiting tokens is where hand-written authentication usually goes wrong.

Machine clients

Integrations that act on their own behalf rather than for a signed-in user use API keys: scoped, hashed at rest, rotated and revoked without touching a password.