Skip to main content

django-api-utility

A reusable Django package for database-configured outbound API integrations. Services and endpoints stay in Django, secret references point to Infisical-injected values, and the request pipeline handles authentication, pooling, retries, validation, caching, refresh, and safe logs.

Supported authentication

Type Credential mode Typical use
NONE Static Public/internal endpoint without authentication
API_KEY Static Key sent in a header or query parameter
BASIC Static Username and password through HTTP Basic
STATIC_BEARER Static Long-lived bearer token
OAUTH2_CLIENT_CREDENTIALS Dynamic OAuth/OIDC machine client, including client_secret_basic, client_secret_post, and public clients
JWT_BEARER_GRANT Dynamic Signed JWT assertion, including Zitadel service users
CUSTOM_TOKEN Dynamic Provider-specific JSON, form, or query token endpoint
SESSION_COOKIE Dynamic Login endpoint that returns one or more cookies

Token requests and normal API requests use a pooled HTTPX client. JWT signing uses the focused joserfc package. Full Authlib is not required because token requests are database-templated and its HTTPX client integration is deprecated.

File upload support is intentionally outside the current scope.

Mutual TLS

mTLS is a separate, process-wide transport setting - not a per-profile AuthType - because it's a client identity the whole service presents, not something that varies per integration. Enable it with MTLS_ENABLED=True plus MTLS_CERT/MTLS_KEY/MTLS_CA (file paths); it combines with any AuthenticationProfile type above, or NONE if the mTLS handshake is the only authentication a given destination needs.

MTLS_ENABLED = True
MTLS_CERT = "/run/secrets/client.crt"
MTLS_KEY = "/run/secrets/client.key"
MTLS_CA = "/run/secrets/ca.crt"

Verified end-to-end against a real mTLS-requiring server (see integration/docker/'s mtls_resource service) - earlier builds of _build_client() passed cert= and verify= to httpx.HTTPTransport as two separate arguments, which httpx silently drops the client certificate for; it now builds one ssl.SSLContext with both the CA and client cert chain loaded together and passes that as verify.

Install

pip install -e .
# Optional only when your custom secret-provider adapter imports infisical-sdk:
pip install -e '.[infisical]'

Add the app and run its migration:

INSTALLED_APPS = [
    # ...
    "django_api_utility",
]
python manage.py migrate django_api_utility

Migration 0009_authentication_profiles adds AuthenticationProfile and optional profile foreign keys to services and endpoints. IntegrationServiceTokenConfig is deprecated as of this migration - nothing in domain/orchestration/transport reads or writes it anymore (confirmed: it's reachable only from admin.py). It's kept, unused, purely so an already-deployed consumer's existing rows stay migration-compatible and visible in admin; there's no compatibility proxy routing new traffic through it. New integrations should use AuthenticationProfile directly.

Version 3.0.0 is the next major release after the published 2.0.0. It changes the response implementation from Requests to HTTPX and adds the authentication-profile architecture, so consumers should treat it as a breaking upgrade.

Data ownership

Store non-secret integration configuration in AuthenticationProfile:

  • token URL and HTTP method
  • request encoding, headers, query, and body templates
  • response extraction rules
  • target API credential placement
  • JWT claims and algorithm settings
  • timeout, scope, and other provider configuration
  • logical secret references

Store values such as API keys, passwords, client secrets, private keys, and static bearer tokens in Infisical. The default provider reads secrets from environment variables, which fits Infisical CLI or Agent injection. A reference can be MY_SECRET, env:MY_SECRET, or {"provider": "env", "name": "MY_SECRET"}.

A direct-SDK Infisical provider ships with the package - django_api_utility.domain.secrets_infisical.InfisicalSecretProvider (Universal Auth machine identity, infisicalsdk's get_secret_by_name; verified end-to-end against a real self-hosted instance - see integration/docker/INFISICAL_VERIFICATION.md):

API_UTILITY_SECRET_PROVIDER = "django_api_utility.domain.secrets_infisical.InfisicalSecretProvider"
INFISICAL_PROJECT_ID = "..."
INFISICAL_ENVIRONMENT_SLUG = "prod"  # default "dev"
# INFISICAL_HOST defaults to https://app.infisical.com; set it for self-hosted.
# INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET (machine identity) come from
# the environment, same as any other deployment secret - never from settings.py.

For anything else, implement an object with get(reference) -> str and configure its dotted path the same way:

API_UTILITY_SECRET_PROVIDER = "my_project.secrets.CustomProvider"

Install the optional infisical dependency (pip install django-api-utility[infisical], the real PyPI distribution is infisicalsdk) to use the shipped adapter. The package does not store retrieved secret values in its models or logs.

Template variables

The request fields use a restricted placeholder resolver. It performs value substitution only and never evaluates Python or general template expressions.

${secret.*}       -> secret provider using secret_references
${config.*}       -> AuthenticationProfile.configuration
${generated.*}    -> generated JWT assertion and similar internal values
${runtime.*}      -> values passed by the caller for this request

An exact placeholder keeps its native JSON type. A placeholder embedded in a longer string is converted to text.

Configuration examples

A static API key in a header:

AuthenticationProfile.objects.create(
    name="vendor-api-key",
    auth_type="API_KEY",
    credential_mode="STATIC",
    secret_references={"api_key": "VENDOR_API_KEY"},
    target_auth_config={
        "location": "header",
        "name": "X-API-Key",
        "format": "{token}",
    },
)

OAuth2 client credentials:

AuthenticationProfile.objects.create(
    name="vendor-oauth",
    auth_type="OAUTH2_CLIENT_CREDENTIALS",
    credential_mode="DYNAMIC",
    token_url="https://identity.example.com/oauth/token",
    configuration={
        "client_id": "machine-client",
        "token_endpoint_auth_method": "client_secret_post",
        "scope": ["read", "write"],
        "token_timeout_seconds": 10,
    },
    secret_references={"client_secret": "VENDOR_CLIENT_SECRET"},
)

The code supplies the normal client-credentials request and response defaults: form encoding, grant_type=client_credentials, access_token, token_type, expires_in, and Authorization: Bearer .... Database values override these defaults.

A Zitadel-style JWT bearer grant:

AuthenticationProfile.objects.create(
    name="zitadel-service-user",
    auth_type="JWT_BEARER_GRANT",
    credential_mode="DYNAMIC",
    token_url="https://identity.example.com/oauth/v2/token",
    configuration={
        "client_id": "service-user-id",
        "scope": ["openid", "urn:zitadel:iam:org:project:id:aud"],
    },
    secret_references={"private_key": "ZITADEL_PRIVATE_KEY_PEM"},
    jwt_config={
        "issuer": "service-user-id",
        "subject": "service-user-id",
        "audience": "https://identity.example.com",
        "key_id": "key-id",
        "algorithm": "RS256",
        "lifetime_seconds": 300,
    },
)

A custom token response and runtime tenant:

AuthenticationProfile.objects.create(
    name="tenant-session",
    auth_type="CUSTOM_TOKEN",
    credential_mode="DYNAMIC",
    token_url="https://login.example.com/${runtime.tenant}/token",
    request_encoding="JSON",
    request_body={
        "username": "${config.username}",
        "password": "${secret.password}",
    },
    configuration={"username": "integration-user"},
    secret_references={"password": "TENANT_API_PASSWORD"},
    response_config={
        "token_source": "json",
        "token_path": "data.access.token",
        "expires_in_path": "data.expires_in",
    },
    target_auth_config={
        "location": "header",
        "name": "Authorization",
        "format": "Bearer {token}",
    },
)

Attach a profile to ExternalServiceConfig for all endpoints. Set ExternalServiceEndpoint.authentication_profile to override it for one endpoint.

Calling endpoints

Provide the existing definition-key JSON mapping through EXTERNAL_DEFINITION_KEYS_FILE, then call:

from django_api_utility import get_by_key, post_by_key

response = get_by_key("users_get", params={"page": 1})
created = post_by_key(
    "users_create",
    payload={"name": "Alice"},
    auth_runtime={"tenant": "acme"},
)

post_by_key, put_by_key, patch_by_key, and request_by_key continue to accept form_data. Existing endpoint fields such as content_type, payload_key_map, and token_flow remain available for compatibility with 2.x registry data.

Authentication query parameters take precedence over caller query parameters so a caller cannot replace a configured credential.

Caching and rotation

Static keys and passwords are read from the secret provider for each resolved request, so an Infisical rotation is used on the next call. Dynamic tokens and cookies are cached until their reported expiry minus refresh_buffer_seconds; a 401 invalidates the cached value, reads current secrets again, and retries once.

Use a shared Django cache such as Redis in multi-worker production deployments. A short cache lock limits simultaneous token fetches across workers, while an in-process lock covers threads. Runtime template values are included in the cache key to prevent credentials from being reused across tenants.

API_UTILITY_AUTH_CACHE_ALIAS = "default"
API_UTILITY_TOKEN_TIMEOUT_SECONDS = 10
API_UTILITY_REQUIRE_HTTPS = True
API_UTILITY_ALLOWED_AUTH_HOSTS = ["identity.example.com", "vendor.example.com"]
API_UTILITY_JWT_ALGORITHMS = ["RS256", "ES256"]

Existing 2.x services without an authentication profile continue to obtain their bearer token through API_UTILITY_MACHINE_TOKEN_DEFINITION_KEY. Assigning a profile switches that service or endpoint to the new direct, template-driven authentication flow.

Logging and failure behavior

The package logs request lifecycle events, upstream status, retry attempts, token refresh results, timeouts, and transport failures with module loggers under django_api_utility. Authorization, cookie, and API-key header values are redacted, and token response bodies are not included in errors. Inactive authentication profiles fail closed.

A typical Django logging configuration is:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "loggers": {
        "django_api_utility": {
            "handlers": ["console"],
            "level": "INFO",
            "propagate": False,
        },
    },
}

Safe HTTP methods retry configured transient status codes with exponential jitter. Transport errors become typed utility exceptions, request and response JSON schemas remain supported, and a 401 triggers one credential refresh before the response returns.

Tests

pip install -e '.[test]'
pytest -q
python -m django makemigrations django_api_utility --check --dry-run

The four-service Docker integration harness runs an authentication provider, an independent protected resource API, a client-certificate-required mTLS resource, and a consumer Django project:

sh integration/docker/generate_test_credentials.sh
docker compose -f integration/docker/compose.yaml up \
  --build --abort-on-container-exit --exit-code-from consumer

It covers every supported AuthenticationProfile type including JWT bearer grant, mTLS (a real client-certificate handshake against mtls_resource, not a mock), all five request methods, local-memory token caching, endpoint overrides, runtime templates, cookies and CSRF, and forced refresh following a 401. Zitadel itself and the Infisical secret provider aren't part of this harness - the latter is heavy enough (a full self-hosted instance plus bootstrap) that it's verified separately; see integration/docker/INFISICAL_VERIFICATION.md for the reproducible steps. See integration/docker/README.md for cleanup instructions.

3.0.0 upgrade notes

  • The distribution and runtime response type move from Requests to HTTPX.
  • Apply migrations 0009 and 0010 after taking the normal database backup. Migration 0010 aligns the database with the 2.0 model state by removing the obsolete session-tracking table and preserving the nullable endpoint content-type behavior.
  • Services without a new authentication profile continue through the 2.x machine-token proxy flow.
  • Assign and test authentication profiles before disabling the legacy proxy configuration.
  • PyJWT is no longer required; JWT assertions use joserfc.

Metadata

Release files for django-api-utility 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-api-utility 3.0.0
File Size Uploaded
django_api_utility-3.0.0.tar.gz 46.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-api-utility 3.0.0
File Interpreter ABI Platform
django_api_utility-3.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 106.1 kB

Release files / django_api_utility-3.0.0.tar.gz

Download URL django_api_utility-3.0.0.tar.gz
Size 46.9 kB
Tags Source
SHA-256 checksum
How to use checksums
37a0527115a62e8feda04cf3b0b63ccd89e1b56a99c1090b529184a9106ae07d
BLAKE2b-256 checksum
How to use checksums
0bff706ee08d2a555b3b9f6038fd5d3ef0965891e42640e098c98df6ab088a56
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / django_api_utility-3.0.0-py3-none-any.whl

Download URL django_api_utility-3.0.0-py3-none-any.whl
Size 59.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7d780850a20951c27f8c8036fead85ee491b5a21eeee5f5bb405d829e883b344
BLAKE2b-256 checksum
How to use checksums
4ac197d6a6e66d35abff5ef29dcb6f4d04dcd5c02f6972bf132ab87f86555ecf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

3.1.0

2 release files

This release

3.0.0 This release

2 release files

2.0.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page