From c156176988d1eb832580a44ee42e8b56fca81769 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 11 Feb 2026 18:40:14 -0500 Subject: [PATCH] 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 - Update test expectations to require 401 for unauthenticated requests - Fix query parameter name from 'query' to 'q' in search endpoints - Update router comments to clarify authentication requirements --- cmd/server/tests/opds_test.go | 13 ++++++------- internal/router/opds.go | 8 ++++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cmd/server/tests/opds_test.go b/cmd/server/tests/opds_test.go index b376aa7..03b6dc0 100644 --- a/cmd/server/tests/opds_test.go +++ b/cmd/server/tests/opds_test.go @@ -23,9 +23,8 @@ func TestOPDSEndpoints(t *testing.T) { require.NoError(t, err) defer resp.Body.Close() - // OPDS endpoints may or may not require auth - depends on implementation - // Accept either 200 (public) or 401 (requires auth) - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized) + // OPDS endpoints require device authentication via devices.auth_token + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) }) t.Run("GetDeviceCatalog_InvalidDeviceID", func(t *testing.T) { @@ -54,7 +53,7 @@ func TestOPDSEndpoints(t *testing.T) { }) t.Run("SearchDeviceCatalog_InvalidDeviceID", func(t *testing.T) { - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/search?query=test", nil) + httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/invalid-uuid/search?q=test", nil) resp, err := client.Do(httpReq) require.NoError(t, err) @@ -66,7 +65,7 @@ func TestOPDSEndpoints(t *testing.T) { t.Run("SearchDeviceCatalog_ValidDevice", func(t *testing.T) { deviceID := uuid.New() - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?query=test", nil) + httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?q=test", nil) httpReq.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(httpReq) @@ -280,7 +279,7 @@ func TestOPDSEdgeCases(t *testing.T) { deviceID := uuid.New() // Search with special characters - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?query=test%20%26%20more", nil) + httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?q=test%20%26%20more", nil) httpReq.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(httpReq) @@ -294,7 +293,7 @@ func TestOPDSEdgeCases(t *testing.T) { t.Run("Search_EmptyQuery", func(t *testing.T) { deviceID := uuid.New() - httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?query=", nil) + httpReq, _ := http.NewRequest("GET", setup.Server.URL+"/opds/devices/"+deviceID.String()+"/search?q=", nil) httpReq.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(httpReq) diff --git a/internal/router/opds.go b/internal/router/opds.go index 9b53f65..615ce99 100644 --- a/internal/router/opds.go +++ b/internal/router/opds.go @@ -1,11 +1,15 @@ package router +// 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 - // OPDS routes (public - device authentication optional) - // Note: OPDSHandler implements its own device authentication + // Require device authentication for all OPDS endpoints opds := e.Group("/opds/devices") + opds.Use(cfg.DeviceAuthMiddleware.Authenticate) opds.GET("/:deviceId/catalog", cfg.OPDSHandler.GetDeviceCatalog) opds.GET("/:deviceId/search", cfg.OPDSHandler.SearchDeviceCatalog) opds.GET("/:deviceId/nav", cfg.OPDSHandler.GetDeviceNavigation)