From b1fcf2ce95f891a17add1f4d2a1eb668c81bf233 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 13 Feb 2026 12:12:36 -0500 Subject: [PATCH] feat: support multiple device authentication methods - Bearer token in Authorization header (KOReader, API clients) - URL path parameter (Kobo sync: /api/sync/kobo/:token/...) - Query parameter (OPDS: ?token=...) - Update Kobo sync routes to use token in path - Add authentication method documentation to OPDS routes --- internal/middleware/device_auth.go | 59 ++++++++++++++++++++++-------- internal/router/opds.go | 12 +++++- internal/router/sync.go | 4 +- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/internal/middleware/device_auth.go b/internal/middleware/device_auth.go index a964def..9cebe04 100644 --- a/internal/middleware/device_auth.go +++ b/internal/middleware/device_auth.go @@ -36,28 +36,57 @@ func NewDeviceAuthMiddleware(db *database.Queries) *DeviceAuthMiddleware { func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { + var device database.Devices + var err error + var token string + var urlToken string + var queryToken string + + // Method 1: Try Bearer token header (KOReader, API clients, OPDS) authHeader := c.Request().Header.Get("Authorization") - if authHeader == "" { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "missing authorization header", - }) + if authHeader != "" { + if !strings.HasPrefix(authHeader, "Bearer ") { + return c.JSON(http.StatusUnauthorized, map[string]string{ + "error": "invalid authorization header format", + }) + } + + token = strings.TrimPrefix(authHeader, "Bearer ") + device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), token) + if err == nil { + // Found device via Bearer token, continue to validation + goto validateDevice + } } - if !strings.HasPrefix(authHeader, "Bearer ") { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "invalid authorization header format", - }) + // Method 2: Try URL path parameter (Kobo sync, OPDS) + // Route format: /api/sync/kobo/:token/... + urlToken = c.Param("token") + if urlToken != "" { + device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), urlToken) + if err == nil { + // Found device via URL path token, continue to validation + goto validateDevice + } } - token := strings.TrimPrefix(authHeader, "Bearer ") - - device, err := m.db.GetDeviceByAuthToken(c.Request().Context(), token) - if err != nil { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "invalid device token", - }) + // Method 3: Try query parameter (OPDS catalog access) + // URL format: /opds/devices/:deviceId/catalog?token=... + queryToken = c.QueryParam("token") + if queryToken != "" { + device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), queryToken) + if err == nil { + // Found device via query parameter token, continue to validation + goto validateDevice + } } + // All authentication methods failed + return c.JSON(http.StatusUnauthorized, map[string]string{ + "error": "authentication required - use Bearer token or API key", + }) + + validateDevice: if !device.SyncEnabled.Bool || !device.SyncEnabled.Valid { return c.JSON(http.StatusForbidden, map[string]string{ "error": "device sync is disabled", diff --git a/internal/router/opds.go b/internal/router/opds.go index 615ce99..32423ed 100644 --- a/internal/router/opds.go +++ b/internal/router/opds.go @@ -1,9 +1,17 @@ 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 +// +// Authentication Methods: +// - Kobo devices: URL path parameter (e.g., /opds/devices/kobo-clara/catalog?token=dev_abc...) +// (Token stored in device for use in stock firmware sync) +// +// - KOReader devices: Bearer token in Authorization header (e.g., Authorization: Bearer dev_xyz...) +// (Token configured in device settings, passed to plugins) +// +// Middleware supports both methods (see device_auth.go) // Returns 401 Unauthorized if device token is missing, invalid, or device sync is disabled + func registerOPDSRoutes(cfg *Config) { e := cfg.Echo diff --git a/internal/router/sync.go b/internal/router/sync.go index fa50e0e..dd0ef0f 100644 --- a/internal/router/sync.go +++ b/internal/router/sync.go @@ -29,8 +29,10 @@ func registerSyncRoutes(cfg *Config) { koreaderSync.POST("/bookmarks", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.SyncBookmarks)) // Kobo sync routes (device authentication required) + // Kobo devices use URL path: /api/sync/kobo/{token}/markup + // API clients can use Authorization header: Authorization: Bearer {token} koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager) - koboSync := e.Group("/api/sync/kobo") + koboSync := e.Group("/api/sync/kobo/:token") koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup)) koboSync.POST("/bookmark", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Bookmark)) koboSync.POST("/v1/analytics/gettests", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.AnalyticsGettests))