Configuration

This document describes how to configure django-indieweb in your Django project.

Django Settings

INDIWEB_AUTH_CODE_TIMEOUT

Controls how long authorization codes remain valid before they must be exchanged for tokens.

Default: 60 (seconds)

Example:

# settings.py
INDIWEB_AUTH_CODE_TIMEOUT = 120  # 2 minutes

Note

Authorization codes are single-use. Once exchanged for a token, they cannot be reused.

INDIEWEB_TOKEN_EXPIRES_IN

Controls how long newly issued or reissued access tokens remain valid.

Default: 86400 (seconds — 24 hours)

Example:

# settings.py
INDIEWEB_TOKEN_EXPIRES_IN = 3600  # 1 hour

The token endpoint reports the remaining lifetime in the expires_in field of its response, and the Micropub endpoint rejects expired tokens with HTTP 401.

Note

Tokens created before this field existed have expires_at set to NULL and continue to be accepted indefinitely until they are reissued. Operators that want to retire those tokens should delete them or trigger a reissue through the IndieAuth flow.

INDIEWEB_CLIENT_ID_VALIDATOR

Optional dotted path to a callable (client_id: str) -> bool that gates which client_id values are allowed by the IndieAuth and Micropub endpoints.

Default: None (every structurally-valid client_id is permitted)

When set, the configured callable is invoked on top of structural validation (an http/https URL with no userinfo and no fragment) at four authorization/token paths — the authorization GET, the consent approval POST, the code-verification POST, and the token endpoint POST — and again on the resource-server path inside TokenAuthMixin. The latter is intentional: operator policy may evolve and revoke a previously-allowed client_id, in which case existing tokens for that client must stop working immediately.

Example:

# myapp/indieauth.py
_ALLOWLIST = {"https://quill.p3k.io/", "https://micropublish.net/"}

def is_allowed_client(client_id: str) -> bool:
    return client_id in _ALLOWLIST
# settings.py
INDIEWEB_CLIENT_ID_VALIDATOR = "myapp.indieauth.is_allowed_client"

Note

If the configured dotted path fails to import, or the callable raises, the request is rejected (fail-closed). Misconfiguration cannot silently weaken access control. The exact response shape depends on the call site: the authorization endpoint and the code-verification POST return HTTP 400 with a plain-text invalid_client body; the token endpoint returns HTTP 400 with body invalid_request and content type application/x-www-form-urlencoded (matching the existing missing- code case); and the Micropub resource-server path returns HTTP 403 with body invalid_client.

Note

Stored client_id values are not re-validated structurally on use, matching the redirect_uri rule. The configured validator callable IS re-applied on use, so revoking a previously-allowed client takes effect immediately for existing tokens. A token issued before client_id access control existed (or by an out-of-band script) keeps working as long as it satisfies the configured validator (or the validator is unset).

URL Configuration

Basic URL Setup

The standard way to include django-indieweb URLs:

# urls.py
from django.urls import path, include

urlpatterns = [
    path('indieweb/', include('indieweb.urls', namespace='indieweb')),
]

This creates the following endpoints:

  • /indieweb/auth/ - Authorization endpoint

  • /indieweb/token/ - Token endpoint

  • /indieweb/micropub/ - Micropub endpoint

Custom URL Paths

You can customize the URL paths:

# urls.py
from indieweb.views import AuthView, TokenView, MicropubView

urlpatterns = [
    path('auth/', AuthView.as_view(), name='indieauth'),
    path('token/', TokenView.as_view(), name='token'),
    path('api/micropub/', MicropubView.as_view(), name='micropub'),
]

Middleware Configuration

CSRF Exemption

The IndieWeb views are automatically exempt from CSRF protection. This is necessary for the token and micropub endpoints to accept POST requests from external clients.

If you need CSRF protection, you’ll need to implement your own views:

from django.views.decorators.csrf import csrf_protect
from indieweb.views import TokenView as BaseTokenView

@method_decorator(csrf_protect, name='dispatch')
class TokenView(BaseTokenView):
    pass

Authentication Backend

django-indieweb uses Django’s standard authentication system. Ensure you have authentication middleware enabled:

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    ...
]

Database Configuration

Models

django-indieweb creates two models:

  1. Auth - Stores authorization codes temporarily

  2. Token - Stores access tokens

Both models use settings.AUTH_USER_MODEL for the user relationship.

Migrations

After installation, run migrations:

python manage.py migrate indieweb

Custom User Model

If using a custom user model, ensure it’s configured before running migrations:

# settings.py
AUTH_USER_MODEL = 'myapp.User'

Security Configuration

HTTPS Requirement

For production, always use HTTPS:

# settings.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

Login URL

Configure where users are redirected for authentication:

# settings.py
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'

Allowed Hosts

Ensure your domain is in ALLOWED_HOSTS:

# settings.py
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

Extending Functionality

Custom Token Model

To add fields to the Token model:

# myapp/models.py
from indieweb.models import Token

class ExtendedToken(Token):
    expires_at = models.DateTimeField(null=True)
    last_used = models.DateTimeField(null=True)

    class Meta:
        db_table = 'indieweb_token'  # Use same table

Custom Views

Extend views to add functionality:

# myapp/views.py
from indieweb.views import TokenView as BaseTokenView
from django.core.cache import cache

class TokenView(BaseTokenView):
    def post(self, request, *args, **kwargs):
        # Add rate limiting
        ip = request.META.get('REMOTE_ADDR')
        cache_key = f'token_attempt_{ip}'
        attempts = cache.get(cache_key, 0)

        if attempts > 5:
            return HttpResponse('Too many attempts', status=429)

        cache.set(cache_key, attempts + 1, 300)  # 5 minutes

        return super().post(request, *args, **kwargs)

Logging Configuration

Enable logging to debug issues:

# settings.py
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': 'indieweb.log',
        },
    },
    'loggers': {
        'indieweb': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

CORS Configuration

For cross-origin requests, install and configure django-cors-headers:

pip install django-cors-headers
# settings.py
INSTALLED_APPS = [
    ...
    'corsheaders',
]

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

# Allow specific origins
CORS_ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://client.example.com",
]

# Or allow all origins (not recommended for production)
CORS_ALLOW_ALL_ORIGINS = True

Testing Configuration

For testing, you might want to disable certain security features:

# test_settings.py
from .settings import *

# Disable HTTPS redirect for tests
SECURE_SSL_REDIRECT = False

# Use a faster password hasher
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]

# Shorter auth code timeout for faster tests
INDIWEB_AUTH_CODE_TIMEOUT = 5

Performance Optimization

Database Indexes

The Token model already has an index on the key field. For better performance with many tokens, consider adding indexes on commonly queried fields:

# In a migration
migrations.AddIndex(
    model_name='token',
    index=models.Index(fields=['owner', 'client_id']),
)

Caching

Cache token lookups for better performance:

# myapp/views.py
from django.core.cache import cache
from indieweb.models import Token

def get_token(key):
    cache_key = f'token_{key}'
    token = cache.get(cache_key)

    if token is None:
        try:
            token = Token.objects.select_related('owner').get(key=key)
            cache.set(cache_key, token, 300)  # Cache for 5 minutes
        except Token.DoesNotExist:
            return None

    return token