Files
thrilltrack-explorer/django-backend/PASSKEY_WEBAUTHN_IMPLEMENTATION_PLAN.md

4.0 KiB

Passkey/WebAuthn Implementation Plan

Status: 🟡 In Progress
Priority: CRITICAL (Required for Phase 2 Authentication)
Estimated Time: 12-16 hours


Overview

Implementing passkey/WebAuthn support to provide modern, passwordless authentication as required by Phase 2 of the authentication migration. This will work alongside existing JWT/password authentication.


Architecture

Backend (Django)

  • WebAuthn Library: webauthn==2.1.0 (already added to requirements)
  • Storage: PostgreSQL models for storing passkey credentials
  • Integration: Works with existing JWT authentication system

Frontend (Next.js)

  • Browser API: Native WebAuthn API (navigator.credentials)
  • Fallback: Graceful degradation for unsupported browsers
  • Integration: Seamless integration with AuthContext

Phase 1: Django Backend Implementation

1.1: Database Models

File: django/apps/users/models.py

class PasskeyCredential(models.Model):
    """
    Stores WebAuthn/Passkey credentials for users.
    """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='passkey_credentials')
    
    # WebAuthn credential data
    credential_id = models.TextField(unique=True, db_index=True)
    credential_public_key = models.TextField()
    sign_count = models.PositiveIntegerField(default=0)
    
    # Metadata
    name = models.CharField(max_length=255, help_text="User-friendly name (e.g., 'iPhone 15', 'YubiKey')")
    aaguid = models.CharField(max_length=36, blank=True)
    transports = models.JSONField(default=list, help_text="Supported transports: ['usb', 'nfc', 'ble', 'internal']")
    
    # Attestation
    attestation_object = models.TextField(blank=True)
    attestation_client_data = models.TextField(blank=True)
    
    # Tracking
    created_at = models.DateTimeField(auto_now_add=True)
    last_used_at = models.DateTimeField(null=True, blank=True)
    is_active = models.BooleanField(default=True)
    
    class Meta:
        db_table = 'users_passkey_credentials'
        ordering = ['-created_at']
        
    def __str__(self):
        return f"{self.user.email} - {self.name}"

1.2: Service Layer

File: django/apps/users/services/passkey_service.py

from webauthn import (
    generate_registration_options,
    verify_registration_response,
    generate_authentication_options,
    verify_authentication_response,
    options_to_json,
)
from webauthn.helpers.structs import (
    AuthenticatorSelectionCriteria,
    UserVerificationRequirement,
    AuthenticatorAttachment,
    ResidentKeyRequirement,
)

class PasskeyService:
    """Service for handling WebAuthn/Passkey operations."""
    
    RP_ID = settings.PASSKEY_RP_ID  # e.g., "thrillwiki.com"
    RP_NAME = "ThrillWiki"
    ORIGIN = settings.PASSKEY_ORIGIN  # e.g., "https://thrillwiki.com"
    
    @staticmethod
    def generate_registration_options(user: User) -> dict:
        """Generate options for passkey registration."""
        
    @staticmethod
    def verify_registration(user: User, credential_data: dict, name: str) -> PasskeyCredential:
        """Verify and store a new passkey credential."""
        
    @staticmethod
    def generate_authentication_options(user: User = None) -> dict:
        """Generate options for passkey authentication."""
        
    @staticmethod
    def verify_authentication(credential_data: dict) -> User:
        """Verify passkey authentication and return user."""
        
    @staticmethod
    def list_credentials(user: User) -> List[PasskeyCredential]:
        """List all passkey credentials for a user."""
        
    @staticmethod
    def remove_credential(user: User, credential_id: str) -> bool:
        """Remove a passkey credential."""

1.3: API Endpoints

File: django/api/v1/endpoints/auth.py (additions)

# Passkey Registration
@router.post("/passkey/register/options", auth=jwt_auth, response={200: dict})