mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 10:05:19 -05:00
- Add new `/profiles/` endpoint for listing user profiles with search, filtering, and pagination support - Implement list_profiles view with OpenAPI documentation for user discovery and leaderboards - Refactor WebAuthn authentication state management to simplify begin_authentication flow - Update MFA passkey login to store user reference instead of full state in cache This enables public profile browsing and improves the passkey authentication implementation by leveraging allauth's internal session management.
17 KiB
17 KiB
Django-Allauth Integration Guide for ThrillWiki
This guide documents how to properly integrate django-allauth for authentication in ThrillWiki, covering JWT tokens, password authentication, MFA (TOTP/WebAuthn), and social OAuth (Google/Discord).
Table of Contents
- Installation & Setup
- JWT Token Authentication
- Password Authentication
- MFA: TOTP (Authenticator App)
- MFA: WebAuthn/Passkeys
- Social OAuth: Google
- Social OAuth: Discord
- API Patterns & DRF Integration
- Internal API Reference
Installation & Setup
Required Packages
# Add packages to pyproject.toml
uv add "django-allauth[headless,mfa,socialaccount]"
uv add fido2 # For WebAuthn support
# Install all dependencies
uv sync
Running Django Commands
# Run migrations
uv run manage.py migrate
# Create superuser
uv run manage.py createsuperuser
# Run development server
uv run manage.py runserver
# Collect static files
uv run manage.py collectstatic
INSTALLED_APPS Configuration
# config/django/base.py
INSTALLED_APPS = [
# Django built-in
"django.contrib.auth",
"django.contrib.sites",
# Allauth core (required)
"allauth",
"allauth.account",
# Optional modules
"allauth.headless", # For headless/API mode
"allauth.mfa", # MFA support (TOTP, recovery codes)
"allauth.mfa.webauthn", # WebAuthn/Passkey support
"allauth.socialaccount", # Social auth base
"allauth.socialaccount.providers.google",
"allauth.socialaccount.providers.discord",
]
Middleware
MIDDLEWARE = [
# ... other middleware
"allauth.account.middleware.AccountMiddleware",
]
URL Configuration
# urls.py
urlpatterns = [
# Allauth browser views (needed for OAuth callbacks)
path("accounts/", include("allauth.urls")),
# Allauth headless API endpoints
path("_allauth/", include("allauth.headless.urls")),
]
JWT Token Authentication
Configuration
# settings.py
# Token strategy - use JWT
HEADLESS_TOKEN_STRATEGY = "allauth.headless.tokens.JWTTokenStrategy"
# Generate private key: openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
HEADLESS_JWT_PRIVATE_KEY = """
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASC...
-----END PRIVATE KEY-----
"""
# Token lifetimes
HEADLESS_JWT_ACCESS_TOKEN_EXPIRES_IN = 300 # 5 minutes
HEADLESS_JWT_REFRESH_TOKEN_EXPIRES_IN = 86400 # 24 hours
# Authorization header scheme
HEADLESS_JWT_AUTHORIZATION_HEADER_SCHEME = "Bearer"
# Stateful validation (invalidates tokens on logout)
HEADLESS_JWT_STATEFUL_VALIDATION_ENABLED = True
# Rotate refresh tokens on use
HEADLESS_JWT_ROTATE_REFRESH_TOKEN = True
DRF Integration
from allauth.headless.contrib.rest_framework.authentication import JWTTokenAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class ProtectedAPIView(APIView):
authentication_classes = [JWTTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({"user": request.user.email})
JWT Flow
- User authenticates (password/social/passkey)
- During auth, pass
X-Session-Tokenheader to allauth API - Upon successful authentication, response
metacontains:{ "meta": { "access_token": "eyJ...", "refresh_token": "abc123..." } } - Use
Authorization: Bearer <access_token>for subsequent requests - Refresh tokens via
POST /_allauth/browser/v1/auth/token/refresh
Password Authentication
Configuration
# config/settings/third_party.py
# Signup fields (* = required)
ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"]
# Login methods
ACCOUNT_LOGIN_METHODS = {"email", "username"} # Allow both
# Email verification
ACCOUNT_EMAIL_VERIFICATION = "mandatory" # Options: "mandatory", "optional", "none"
ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True
ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True
# Security
ACCOUNT_REAUTHENTICATION_REQUIRED = True # Require re-auth for sensitive operations
ACCOUNT_EMAIL_NOTIFICATIONS = True
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False # Don't reveal if email exists
# Redirects
LOGIN_REDIRECT_URL = "/"
ACCOUNT_LOGOUT_REDIRECT_URL = "/"
# Custom adapters
ACCOUNT_ADAPTER = "apps.accounts.adapters.CustomAccountAdapter"
Headless API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/_allauth/browser/v1/auth/login |
POST | Login with email/username + password |
/_allauth/browser/v1/auth/signup |
POST | Register new account |
/_allauth/browser/v1/auth/logout |
POST | Logout (invalidate tokens) |
/_allauth/browser/v1/auth/password/reset |
POST | Request password reset |
/_allauth/browser/v1/auth/password/reset/key |
POST | Complete password reset |
/_allauth/browser/v1/auth/password/change |
POST | Change password (authenticated) |
Magic Link (Login by Code)
# Enable magic link authentication
ACCOUNT_LOGIN_BY_CODE_ENABLED = True
ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS = 3
ACCOUNT_LOGIN_BY_CODE_TIMEOUT = 300 # 5 minutes
MFA: TOTP (Authenticator App)
Configuration
# config/settings/third_party.py
# Enable TOTP in supported types
MFA_SUPPORTED_TYPES = ["totp", "webauthn"]
# TOTP settings
MFA_TOTP_ISSUER = "ThrillWiki" # Shows in authenticator app
MFA_TOTP_DIGITS = 6 # Code length
MFA_TOTP_PERIOD = 30 # Seconds per code
TOTP API Flow
-
Get TOTP Secret (for QR code):
GET /_allauth/browser/v1/account/authenticators/totpResponse contains
totp_urlfor QR code generation. -
Activate TOTP:
POST /_allauth/browser/v1/account/authenticators/totp { "code": "123456" } -
Deactivate TOTP:
DELETE /_allauth/browser/v1/account/authenticators/totp -
MFA Login Flow:
- After password auth, if MFA enabled, receive
401withmfa_required - Submit TOTP code:
POST /_allauth/browser/v1/auth/2fa/authenticate { "code": "123456" }
- After password auth, if MFA enabled, receive
MFA: WebAuthn/Passkeys
Configuration
# config/settings/third_party.py
# Include webauthn in supported types
MFA_SUPPORTED_TYPES = ["totp", "webauthn"]
# Enable passkey-only login
MFA_PASSKEY_LOGIN_ENABLED = True
# Allow insecure origin for localhost development
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = True # Only for DEBUG=True
Internal WebAuthn API Functions
The allauth.mfa.webauthn.internal.auth module provides these functions:
from allauth.mfa.webauthn.internal import auth as webauthn_auth
# Registration Flow
def begin_registration(user, passwordless: bool) -> Dict:
"""
Start passkey registration.
Args:
user: The Django user object
passwordless: True for passkey login, False for MFA-only
Returns:
Dict with WebAuthn creation options (challenge, rp, user, etc.)
Note: State is stored internally via set_state()
"""
def complete_registration(credential: Dict) -> AuthenticatorData:
"""
Complete passkey registration.
Args:
credential: The parsed credential response from browser
Returns:
AuthenticatorData (binding) - NOT an Authenticator model
Note: You must create the Authenticator record yourself!
"""
# Authentication Flow
def begin_authentication(user=None) -> Dict:
"""
Start passkey authentication.
Args:
user: Optional user (for MFA). None for passwordless login.
Returns:
Dict with WebAuthn request options
Note: State is stored internally via set_state()
"""
def complete_authentication(user, response: Dict) -> Authenticator:
"""
Complete passkey authentication.
Args:
user: The Django user object
response: The credential response from browser
Returns:
The matching Authenticator model instance
"""
# State Management (internal, use context)
def get_state() -> Optional[Dict]:
"""Get stored WebAuthn state from session."""
def set_state(state: Dict) -> None:
"""Store WebAuthn state in session."""
def clear_state() -> None:
"""Clear WebAuthn state from session."""
# Helper functions
def parse_registration_response(response: Any) -> RegistrationResponse:
"""Parse browser registration response."""
def parse_authentication_response(response: Any) -> AuthenticationResponse:
"""Parse browser authentication response."""
Custom Passkey API Implementation
# apps/api/v1/auth/passkey.py
from allauth.mfa.webauthn.internal import auth as webauthn_auth
from allauth.mfa.models import Authenticator
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_registration_options(request):
"""Get WebAuthn registration options."""
# passwordless=False for MFA passkeys, True for passwordless login
creation_options = webauthn_auth.begin_registration(
request.user,
passwordless=False
)
# State is stored internally
return Response({"options": creation_options})
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def register_passkey(request):
"""Complete passkey registration."""
credential = request.data.get("credential")
name = request.data.get("name", "Passkey")
# Check for pending registration
state = webauthn_auth.get_state()
if not state:
return Response({"error": "No pending registration"}, status=400)
# Parse and complete registration
credential_data = webauthn_auth.parse_registration_response(credential)
authenticator_data = webauthn_auth.complete_registration(credential_data)
# Create Authenticator record manually
authenticator = Authenticator.objects.create(
user=request.user,
type=Authenticator.Type.WEBAUTHN,
data={"name": name},
)
return Response({"id": str(authenticator.id), "name": name})
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def get_authentication_options(request):
"""Get WebAuthn authentication options."""
request_options = webauthn_auth.begin_authentication(request.user)
return Response({"options": request_options})
@api_view(["POST"])
@permission_classes([IsAuthenticated])
def authenticate_passkey(request):
"""Verify passkey authentication."""
credential = request.data.get("credential")
state = webauthn_auth.get_state()
if not state:
return Response({"error": "No pending authentication"}, status=400)
# Complete authentication (handles state internally)
webauthn_auth.complete_authentication(request.user, credential)
return Response({"success": True})
Social OAuth: Google
Google Cloud Console Setup
- Go to Google Cloud Console
- Create/select project → APIs & Services → Credentials
- Create OAuth 2.0 Client ID (Web application)
- Set Authorized JavaScript origins:
http://localhost:3000(development)https://thrillwiki.com(production)
- Set Authorized redirect URIs:
http://localhost:8000/accounts/google/login/callback/https://api.thrillwiki.com/accounts/google/login/callback/
- Note the Client ID and Client Secret
Django Configuration
# config/settings/third_party.py
SOCIALACCOUNT_PROVIDERS = {
"google": {
"SCOPE": ["profile", "email"],
"AUTH_PARAMS": {"access_type": "online"}, # Use "offline" for refresh tokens
"OAUTH_PKCE_ENABLED": True,
# "FETCH_USERINFO": True, # If you need avatar_url for private profiles
},
}
Admin Setup
- Go to
/admin/socialaccount/socialapp/ - Add new Social Application:
- Provider: Google
- Name: Google
- Client ID: (from Google Console)
- Secret key: (from Google Console)
- Sites: Select your site
Social OAuth: Discord
Discord Developer Portal Setup
- Go to Discord Developer Portal
- Create New Application
- Go to OAuth2 → General
- Add Redirect URIs:
http://127.0.0.1:8000/accounts/discord/login/callback/https://api.thrillwiki.com/accounts/discord/login/callback/
- Note Client ID and Client Secret
Django Configuration
# config/settings/third_party.py
SOCIALACCOUNT_PROVIDERS = {
"discord": {
"SCOPE": ["identify", "email"], # "identify" is required
"OAUTH_PKCE_ENABLED": True,
},
}
Admin Setup
- Go to
/admin/socialaccount/socialapp/ - Add new Social Application:
- Provider: Discord
- Name: Discord
- Client ID: (from Discord Portal)
- Secret key: (from Discord Portal)
- Sites: Select your site
API Patterns & DRF Integration
Authentication Classes
from allauth.headless.contrib.rest_framework.authentication import (
JWTTokenAuthentication,
SessionTokenAuthentication,
)
# For JWT-based authentication
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"allauth.headless.contrib.rest_framework.authentication.JWTTokenAuthentication",
],
}
Headless Frontend URLs
# Required for email verification, password reset links
HEADLESS_FRONTEND_URLS = {
"account_confirm_email": "https://thrillwiki.com/account/verify-email/{key}",
"account_reset_password_from_key": "https://thrillwiki.com/account/password/reset/{key}",
"account_signup": "https://thrillwiki.com/signup",
}
Custom Adapters
# apps/accounts/adapters.py
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class CustomAccountAdapter(DefaultAccountAdapter):
def save_user(self, request, user, form, commit=True):
"""Customize user creation."""
user = super().save_user(request, user, form, commit=False)
# Custom logic here
if commit:
user.save()
return user
def is_open_for_signup(self, request):
"""Control signup availability."""
return True
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
"""Hook before social login completes."""
# Link social account to existing user by email
if sociallogin.is_existing:
return
email = sociallogin.account.extra_data.get("email")
if email:
User = get_user_model()
try:
user = User.objects.get(email=email)
sociallogin.connect(request, user)
except User.DoesNotExist:
pass
Internal API Reference
Authenticator Model
from allauth.mfa.models import Authenticator
# Types
Authenticator.Type.TOTP # TOTP authenticator
Authenticator.Type.WEBAUTHN # WebAuthn/Passkey
Authenticator.Type.RECOVERY_CODES # Recovery codes
# Query user's authenticators
passkeys = Authenticator.objects.filter(
user=user,
type=Authenticator.Type.WEBAUTHN
)
# Check if MFA is enabled
from allauth.mfa.adapter import get_adapter
is_mfa_enabled = get_adapter().is_mfa_enabled(user)
Session Token Header
For headless mode during authentication flow:
X-Session-Token: <session-token>
After authentication completes with JWT enabled:
Authorization: Bearer <access-token>
Current ThrillWiki Implementation Summary
ThrillWiki already has these allauth features configured:
| Feature | Status | Notes |
|---|---|---|
| Password Auth | ✅ Configured | Email + username login |
| Email Verification | ✅ Mandatory | With resend support |
| TOTP MFA | ✅ Configured | 6-digit codes, 30s period |
| WebAuthn/Passkeys | ✅ Configured | Passkey login enabled |
| Google OAuth | ✅ Configured | Needs admin SocialApp |
| Discord OAuth | ✅ Configured | Needs admin SocialApp |
| Magic Link | ✅ Configured | 5-minute timeout |
| JWT Tokens | ❌ Not configured | Using SimpleJWT instead |
Recommendation
To use allauth's native JWT support instead of SimpleJWT:
- Add
"allauth.headless"to INSTALLED_APPS - Configure
HEADLESS_TOKEN_STRATEGYand JWT settings - Replace
rest_framework_simplejwtauthentication withJWTTokenAuthentication - Add
/_allauth/URL routes