Files
bookhoard/fix-opds-device-authentication.md
T

5.6 KiB

Fix OPDS Device Authentication

Problem Summary

Current State:

  • OPDS routes (/opds/devices/*) are public - no authentication
  • Device sync API (/api/sync/kobo/*, /api/sync/koreader/*) uses DeviceAuthMiddleware → validates devices.auth_token
  • Anyone can enumerate/access device UUIDs without authentication
  • Test GetDeviceCatalog_WithoutDeviceAuth expects 401 (auth required) but gets 404 (device not found)

Design Principle: Devices belong to users; OPDS access should require device authentication via devices.auth_token (same model as sync API).


Step-by-Step Implementation Plan

Step 1: Apply DeviceAuthMiddleware to OPDS Routes

File: internal/router/opds.go

Changes:

func registerOPDSRoutes(cfg *Config) {
    e := cfg.Echo

    // Apply device authentication middleware
    opds := e.Group("/opds/devices")
    opds.Use(cfg.DeviceAuthMiddleware.Authenticate)  // ← ADD THIS
    
    // OPDS routes now require valid devices.auth_token
    opds.GET("/:deviceId/catalog", cfg.OPDSHandler.GetDeviceCatalog)
    opds.GET("/:deviceId/search", cfg.OPDSHandler.SearchDeviceCatalog)
    opds.GET("/:deviceId/nav", cfg.OPDSHandler.GetDeviceNavigation)
    opds.GET("/:deviceId/download/:bookId", cfg.OPDSHandler.DownloadBook)
    opds.GET("/:deviceId/cover/:bookId", cfg.OPDSHandler.GetCoverImage)
    opds.GET("/:deviceId/formats/:bookId", cfg.OPDSHandler.ListFormats)
}

Rationale:

  • Reuses existing authentication pattern (proven on sync endpoints)
  • Middleware validates devices.auth_token from Authorization: Bearer {token} header
  • Returns 401 if missing/invalid/expired token
  • Device must belong to user (already enforced in device table)

Step 2: Update OPDS Handler Comments

File: internal/router/opds.go

Change:

// Register OPDS routes with device authentication
// Devices must use their devices.auth_token (generated during device registration/approval)
// Kobo devices store this token for both sync and OPDS catalog access
// Returns 401 Unauthorized if device token is missing, invalid, or device sync is disabled
func registerOPDSRoutes(cfg *Config) {
    e := cfg.Echo

    // Require device authentication for all OPDS endpoints
    opds := e.Group("/opds/devices")
    opds.Use(cfg.DeviceAuthMiddleware.Authenticate)
    ...
}

Remove misleading comment:

- // OPDS routes (public - device authentication optional)
- // Note: OPDSHandler implements its own device authentication

Step 3: Update Test Expectations

File: cmd/server/tests/opds_test.go

Change 1 - Remove invalid 200 option:

// Line 18-29: GetDeviceCatalog_WithoutDeviceAuth
t.Run("GetDeviceCatalog_WithoutDeviceAuth", func(t *testing.T) {
    deviceID := uuid.New()
    httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/catalog", nil)

    resp, err := client.Do(httpReq)
    require.NoError(t, err)
    defer resp.Body.Close()

    // OPDS endpoints require device authentication
    assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)  // ← Expect 401 only
})

Change 2 - Update comment:

// Test: OPDS endpoints require device authentication via devices.auth_token

Step 4: Update Additional OPDS Edge Case Test

File: cmd/server/tests/opds_test.go

Change: Update TestOPDSEdgeCases/Search_SpecialCharacters to use auth token:

t.Run("Search_SpecialCharacters", func(t *testing.T) {
    deviceID := uuid.New()

    // Search with special characters (requires valid device auth token)
    httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?q=test%20%26%20more", nil)
    httpReq.Header.Set("Authorization", "Bearer "+setup.DeviceToken)  // ← ADD THIS

Step 5: Update Router Comment

File: internal/router/opds.go

Add documentation:

// Register OPDS routes with device authentication
// Devices must use their devices.auth_token (generated during device registration/approval)
// Kobo devices store this token for both sync and OPDS catalog access
// Returns 401 Unauthorized if device token is missing, invalid, or device sync is disabled
func registerOPDSRoutes(cfg *Config) {
    e := cfg.Echo

    // Require device authentication for all OPDS endpoints
    opds := e.Group("/opds/devices")
    opds.Use(cfg.DeviceAuthMiddleware.Authenticate)
    ...
}

Test Verification Plan

After implementing changes, verify:

  1. GetDeviceCatalog_WithoutDeviceAuth - Should return 401 (no auth token provided)
  2. GetDeviceCatalog_ValidDevice - With setup.DeviceToken should return 200 (valid device)
  3. SearchDeviceCatalog_ValidDevice - With device token should return 200 or 404 (no results)
  4. TestOPDSEdgeCases - All should use setup.DeviceToken for authentication

Git Commit Structure

Commit 1: Apply DeviceAuthMiddleware to OPDS routes

fix(opds): Require device authentication for OPDS catalog endpoints

- Apply DeviceAuthMiddleware.Authenticate to /opds/devices/* routes
- OPDS now uses same authentication model as sync API (devices.auth_token)
- Removes security vulnerability allowing unauthorized device enumeration
- Updates router comments to clarify authentication requirements

Refs: #PROJECT_GUIDELINES.md

Commit 2: Update OPDS test expectations

test(opds): Update test expectations for device authentication

- GetDeviceCatalog_WithoutDeviceAuth: Expect 401 (auth required)
- TestOPDSEdgeCases: Use setup.DeviceToken for authenticated requests
- Remove misleading "public or 401" test expectations

Refs: #PROJECT_GUIDELINES.md