docs / django middleware

Django middleware

Gate whole URL patterns with a small request middleware, or individual views with a decorator.

Install

pip install django orangecheck

Option 1: middleware

# myapp/middleware.py
from django.http import JsonResponse
from orangecheck import check, OrangeCheckError

class OrangeCheckMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.path.startswith("/protected/"):
            addr = request.session.get("verified_btc_address")
            try:
                r = check(addr=addr, min_sats=100_000, min_days=30)
            except OrangeCheckError as e:
                return JsonResponse({"error": str(e)}, status=500)
            if not r.ok:
                return JsonResponse(
                    {"error": "orangecheck", "reasons": list(r.reasons)},
                    status=403,
                )
            request.orangecheck = r
        return self.get_response(request)

Register in settings.py:

MIDDLEWARE = [
    # ... other middleware
    "myapp.middleware.OrangeCheckMiddleware",
]

Then in views:

def protected_view(request):
    sats = request.orangecheck.sats
    return JsonResponse({"sats": sats})

Option 2: decorator

# myapp/decorators.py
from functools import wraps
from django.http import JsonResponse
from orangecheck import check

def require_orangecheck(min_sats=100_000, min_days=30):
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(request, *args, **kwargs):
            addr = request.session.get("verified_btc_address")
            r = check(addr=addr, min_sats=min_sats, min_days=min_days)
            if not r.ok:
                return JsonResponse(
                    {"error": "orangecheck", "reasons": list(r.reasons)},
                    status=403,
                )
            request.orangecheck = r
            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator

Usage:

@require_orangecheck(min_sats=100_000, min_days=30)
def protected_view(request):
    return JsonResponse({"ok": True})

Signed-challenge auth

For the full "sign in with Bitcoin" flow in Django:

# urls.py
urlpatterns = [
    path("auth/challenge", challenge_issue_view),
    path("auth/verify",    challenge_verify_view),
]

# views.py
from orangecheck import challenge_issue, challenge_verify

def challenge_issue_view(request):
    addr = request.GET.get("addr")
    c = challenge_issue(addr=addr, audience="https://example.com", purpose="login")
    request.session["oc_nonce"] = c.nonce
    return JsonResponse({"message": c.message})

def challenge_verify_view(request):
    body = json.loads(request.body)
    try:
        r = challenge_verify(
            message=body["message"],
            signature=body["signature"],
            expected_nonce=request.session.get("oc_nonce"),
            expected_audience="https://example.com",
        )
    except VerificationError as e:
        return JsonResponse({"error": str(e)}, status=401)
    request.session["verified_btc_address"] = r.address
    return JsonResponse({"ok": True, "address": r.address})

Now request.session["verified_btc_address"] is cryptographically proven on subsequent requests.

Async views (Django 4.1+)

Use AsyncClient:

from orangecheck import AsyncClient

oc = AsyncClient()

async def async_protected_view(request):
    addr = request.session.get("verified_btc_address")
    r = await oc.check(addr=addr, min_sats=100_000)
    if not r.ok:
        return JsonResponse({"error": r.reasons}, status=403)
    return JsonResponse({"sats": r.sats})

Further