Token-based authorization middleware for protecting Django URL paths.
uv add django-magic-authorization
or
pip install django-magic-authorization
Add the app and middleware to your Django settings:
# settings.py
INSTALLED_APPS = [
...,
"django_magic_authorization",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django_magic_authorization.middleware.MagicAuthorizationMiddleware",
...,
]Place MagicAuthorizationMiddleware after SecurityMiddleware.
Run migrations:
python manage.py migrate
Mark any URL as protected using protected_path, a drop-in replacement for
django.urls.path:
# urls.py
from django.http import HttpResponse
from django_magic_authorization.urls import protected_path
def secret_view(request):
return HttpResponse("Secret content")
urlpatterns = [
protected_path("secret/", secret_view),
]Accessing /secret/ without a token returns 403. Create a token to grant
access:
from django_magic_authorization.models import AccessToken
token = AccessToken.objects.create(path="secret/", description="Demo token")
print(token.token)
# e.g. "aB3x..."Visit /secret/?token=aB3x... to authenticate. The token is stripped from the
URL via redirect, and a cookie is set for subsequent requests.
protected_path supports the same route syntax as django.urls.path,
including captured parameters:
urlpatterns = [
protected_path("blog/<int:year>/<str:slug>/", blog_detail_view),
]A token with path="blog/<int:year>/<str:slug>/" will protect all URLs
matching that pattern.
Use protected_path with include() to protect an entire URL subtree:
from django.urls import include
from django_magic_authorization.urls import protected_path
urlpatterns = [
protected_path("internal/", include("internal.urls")),
]All paths under /internal/ are protected with a single token.
Pass a protect_fn callable to protect URLs conditionally based on captured
parameters. It receives the captured kwargs dict and should return True if the
path should be protected:
urlpatterns = [
protected_path(
"<str:visibility>/<int:pk>/",
detail_view,
protect_fn=lambda kwargs: kwargs["visibility"] == "private",
),
]Here /private/42/ requires a token, but /public/42/ does not.
from django_magic_authorization.models import AccessToken
token = AccessToken.objects.create(
path="secret/",
description="For reviewers",
)Tokens are generated using secrets.token_urlsafe(32).
token.is_valid = False
token.save()from django.utils import timezone
from datetime import timedelta
AccessToken.objects.create(
path="secret/",
description="Expires in 7 days",
expires_at=timezone.now() + timedelta(days=7),
)AccessToken.objects.create(
path="secret/",
description="Single use",
max_uses=1,
)Each token tracks times_accessed and last_accessed automatically.
Remove expired and exhausted tokens:
python manage.py cleanup_expired_tokens
On first valid token access, a cookie is set so subsequent requests to the same path do not require the token in the URL. Cookies are scoped to the static prefix of the protected path pattern (everything before the first dynamic segment). Cookie attributes are configurable via settings.
Override the default 403 response with a template or a handler function.
Template:
MAGIC_AUTHORIZATION = {
"FORBIDDEN_TEMPLATE": "errors/403.html",
}The template receives path in its context.
Handler function:
MAGIC_AUTHORIZATION = {
"FORBIDDEN_HANDLER": "myapp.views.custom_forbidden",
}The handler is called as handler(request, path) and must return an
HttpResponse.
Two signals are available for monitoring access:
access_granted -- sent after successful token validation.
from django_magic_authorization.signals import access_granted
def on_access_granted(sender, request, token, path, **kwargs):
...
access_granted.connect(on_access_granted)Keyword arguments: sender (AccessToken class), request, token
(AccessToken instance), path.
access_denied -- sent when access is denied.
from django_magic_authorization.signals import access_denied
def on_access_denied(sender, request, path, reason, **kwargs):
...
access_denied.connect(on_access_denied)Keyword arguments: sender (None), request, path, reason
("no_token" or "invalid_token").
Register the app to get a management interface for access tokens. The admin displays all token fields, provides a dropdown of registered protected paths when creating tokens, and shows a computed access link for each token. Tokens with paths that no longer match a registered route are flagged in the list view.
All settings are namespaced under MAGIC_AUTHORIZATION in your Django settings:
MAGIC_AUTHORIZATION = {
"COOKIE_SECURE": True,
"COOKIE_MAX_AGE": 86400,
}| Key | Default | Description |
|---|---|---|
COOKIE_SECURE |
not DEBUG |
Set the Secure flag on auth cookies |
COOKIE_MAX_AGE |
31536000 (1 year) |
Cookie max age in seconds |
COOKIE_SAMESITE |
"lax" |
Cookie SameSite attribute |
COOKIE_HTTPONLY |
True |
Set the HttpOnly flag on auth cookies |
COOKIE_PREFIX |
"django_magic_authorization_" |
Prefix for cookie names |
TOKEN_PARAM |
"token" |
Query parameter name for the token |
FORBIDDEN_TEMPLATE |
None |
Template path for custom 403 page |
FORBIDDEN_HANDLER |
None |
Dotted path to a custom 403 handler function |
- Use HTTPS in production.
COOKIE_SECUREdefaults toTruewhenDEBUGisFalse. - Tokens are automatically stripped from URLs via redirect after first use, preventing token leakage in browser history and referrer headers.
- Auth cookies are
HttpOnlyandSameSite=laxby default. - Tokens are generated with
secrets.token_urlsafe(32)(256 bits of entropy).
This project uses AI-assisted development tools. See the AI usage policy for details.
Tools
- Claude Code (Anthropic) Β·
claude-sonnet-4-6Β· Agentic
Phase Humanβ AI
ββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββ
Requirements & Scope 95% βββββββββββ 5%
Architecture & Design 85% βββββββββββ 15%
Implementation 40% βββββββββββ 60%
Testing 20% βββββββββββ 80%
Documentation 5% βββββββββββ 95%
Oversight: Collaborative
Human and AI co-author decisions; human reviews all output.
AI agent operated autonomously across multi-step tasks. Human reviewed diffs, resolved conflicts, and approved merges.
The human author(s) are solely responsible for the content, accuracy, and fitness-for-purpose of this project.
Last updated: 2026-02-20 Β· Generated with ai-disclaimer