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
This commit is contained in:
2026-02-11 18:40:14 -05:00
parent d07142917c
commit c156176988
2 changed files with 12 additions and 9 deletions
+6 -7
View File
@@ -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)
+6 -2
View File
@@ -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)