# ADR 0017: Backend-Owned Sessions with Django Auth + OAuth via django-allauth - **Status:** Accepted - **Date:** 2025-01-15 - **Deciders:** Core architecture team - **Related:** ADR 0001 (Django/DRF), ADR 0002 (Next.js), ADR 0014 (RBAC), docs/architecture/10-api-design.md ## Context The system has a Next.js frontend and a Django API (ADRs 0001/0002), plus a **public API** for third-party clients. Authentication must serve three consumers: 1. The first-party web app (browser). 2. Third-party API clients (scripts, integrations, future mobile apps). 3. The community's self-hosting reality: small instances must not depend on paid identity providers; everything must work with email/password alone, with social login optional. The architectural fork in the road: **who owns identity** — Django (with the frontend as a client) or the Next.js layer via Auth.js (with Django trusting tokens minted by the frontend)? Security context: this platform stores reputations, review powers, and moderation capabilities — account takeover has governance consequences, not just personal ones. We also operate sandboxed third-party widgets (ADR 0009) in the same product, so browser-side credential exposure must be minimized. ## Decision **Django owns identity end-to-end.** Specifics: 1. **Identity provider:** Django's auth system + **django-allauth** for registration, email verification, password reset, and OAuth social login (GitHub and Google configured by default; any allauth-supported provider available to self-hosters by configuration). Social accounts link to a local user record; email/password always works with no external dependency. 2. **First-party web sessions: HTTP-only cookies, not JWTs.** - Django session cookie: `HttpOnly`, `Secure`, `SameSite=Lax`, served on the API origin. - CSRF protection via Django's CSRF token, exposed to the SPA through the standard cookie-to-header pattern (`X-CSRFToken`). - The Next.js app calls the API same-site (API served on `api.` or path-proxied `/api` on the same registrable domain), so cookies flow without third-party-cookie problems. - **No tokens in JavaScript-accessible storage, ever.** With sandboxed widget iframes and community content in the product, keeping credentials out of script reach is non-negotiable; `HttpOnly` cookies make token exfiltration via any XSS-class bug categorically harder. - Server-rendered Next.js pages needing personalized data forward the incoming cookie to the API on the server side (standard BFF pattern); no second session store exists in the Next.js layer. 3. **Sessions stored in PostgreSQL** (`django.contrib.sessions.backends.db` with the cached_db wrapper over Redis for read performance). Revocation is immediate and global: deleting the session row kills the session — a property stateless JWTs cannot offer without a denylist, and which matters when a moderator account is compromised. 4. **Third-party API access: personal access tokens (PATs), not OAuth2-provider machinery (yet).** - Users mint named, scoped tokens in settings (`read:content`, `write:attempts`, `read:profile` scopes for MVP); tokens are random 256-bit values, stored **hashed** (SHA-256), shown once at creation, revocable individually, with last-used tracking. - Sent as `Authorization: Bearer `. DRF authentication classes accept *either* session+CSRF (browser) *or* PAT (programmatic); policy enforcement (ADR 0014) is identical downstream. - Becoming a full OAuth2/OIDC *provider* (third-party apps acting on behalf of users with consent screens) is explicitly deferred; PATs cover MVP integration needs at a fraction of the attack surface. 5. **Account security baseline:** mandatory email verification before contribution actions (anti-spam), Django password validators tuned to NIST-style length-first rules, login throttling via Redis rate-limits, session rotation on privilege change, and **TOTP two-factor (django-allauth's MFA support) required for moderator/admin role holders** — enforced at role-grant time, not optional. All auth events (login, failed login burst, password change, MFA enrollment, token creation/revocation) emit audit entries (ADR 0013) where governance-relevant, and structured logs otherwise. ## Alternatives Considered - **Auth.js (NextAuth) owning identity:** attractive for Next.js-heavy teams, but it inverts trust — Django (which enforces all policy, ADR 0014) would validate identities minted elsewhere, identity would live in the Node layer while roles/audit live in Django, and self-hosters would configure auth in two places. The public API would still need Django-side credentials anyway. Rejected: identity belongs next to authorization and audit. - **JWT access/refresh tokens for the first-party app:** stateless scaling benefits are irrelevant at our scale, while the costs are real: revocation complexity, refresh-token storage in the browser, and token theft surface. Rejected for first-party; PATs (which are opaque server-validated tokens, not JWTs) cover programmatic use. - **Full OAuth2 provider (django-oauth-toolkit) at MVP:** real machinery, real attack surface (redirect URI validation, consent UX, grant types) serving a third-party-app ecosystem that doesn't exist yet. Deferred with a clear trigger: first credible third-party app request. - **External IdP (Keycloak/Ory):** powerful, but an entire additional service with its own database and upgrade lifecycle — disproportionate for self-hosters. Federation needs, if they arrive, can be met through allauth's OIDC client support. ## Consequences **Positive** - One identity system, co-located with policy and audit; immediate revocation; zero scriptable credentials in the browser; works fully offline-from-third-parties for self-hosters. - PATs give the public API a simple, secure, scoped credential story from day one. **Negative / Accepted risks** - Cookie auth requires same-site API topology; deployment docs must be explicit about the `api.` subdomain / proxy-path requirement and CORS/CSRF configuration. This is the most common self-hosting misconfiguration we expect; we'll ship a startup-time configuration self-check that detects mismatched origins. - CSRF handling adds frontend plumbing (one fetch wrapper) versus bearer tokens. Minor, one-time. - A future native mobile app prefers tokens over cookies; PAT-style long-lived tokens or the deferred OAuth provider covers this when it materializes. **Follow-ups** - Backend milestone: allauth configuration, PAT model + hashing + scopes, MFA-required-for-elevated-roles enforcement hook, origin self-check. - Document the cookie/CSRF fetch wrapper in the frontend milestone.