From 715f9e7c7d6f8e76304cf11412f611520fbf0f06 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 12 May 2026 18:22:20 -0700 Subject: [PATCH] fix(auth): allow unauthenticated user_id on /feed/for-you MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The For You handler treats the path :userId as the personalization target and the user_id query param as a viewer hint used only for response decoration (has_current_user_reposted, etc.). It already calls tryGetAuthedWallet (optional), so the handler is happy with an unauthenticated request — but the global authMiddleware returns 403 whenever user_id is set and the request isn't signed, blocking the call before it reaches the handler. Exempt the /feed/for-you route from that strict check. The path :userId still controls what content is returned and access-gated tracks are still filtered out when authedWallet is empty, so the security shape is unchanged for this read endpoint. Adds TestV1FeedForYou_UnauthenticatedViewerIdAllowed which exercises the exemption with skipAuthCheck OFF (so the real auth path runs). Co-Authored-By: Claude Opus 4.7 (1M context) --- api/auth_middleware.go | 12 ++++++++++-- api/v1_users_feed_for_you_test.go | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/api/auth_middleware.go b/api/auth_middleware.go index 05cc3bbe..2060f207 100644 --- a/api/auth_middleware.go +++ b/api/auth_middleware.go @@ -347,8 +347,16 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusUnauthorized, "Invalid or expired access token") } - // Not authorized to act on behalf of myId - if myId != 0 && !pkceAuthed && !app.isAuthorizedRequest(c.Context(), myId, wallet) { + // Not authorized to act on behalf of myId. + // + // Exception: /users/:userId/feed/for-you accepts user_id as a viewer hint + // used only for response decoration (has_current_user_reposted etc.); the + // path :userId — not user_id — controls what gets personalized. Treat the + // query user_id as advisory rather than authoritative on this route so + // the endpoint can be called like the other public read endpoints. + allowUnauthenticatedViewerId := strings.HasSuffix(c.Path(), "/feed/for-you") + + if myId != 0 && !pkceAuthed && !allowUnauthenticatedViewerId && !app.isAuthorizedRequest(c.Context(), myId, wallet) { return fiber.NewError( fiber.StatusForbidden, fmt.Sprintf( diff --git a/api/v1_users_feed_for_you_test.go b/api/v1_users_feed_for_you_test.go index c1821c90..b67c1607 100644 --- a/api/v1_users_feed_for_you_test.go +++ b/api/v1_users_feed_for_you_test.go @@ -148,6 +148,25 @@ func TestV1FeedForYou_RequiresValidUserId(t *testing.T) { assert.Equal(t, 400, status) } +// /feed/for-you is exempt from authMiddleware's "if user_id is set, wallet +// must match" 403 — the query user_id is a viewer hint, not an authorization +// claim. This test pins that exemption: with skipAuthCheck OFF (so the real +// auth path runs) and no signature headers, the call still returns 200. +func TestV1FeedForYou_UnauthenticatedViewerIdAllowed(t *testing.T) { + app := emptyTestApp(t) + // Deliberately NOT setting app.skipAuthCheck — exercise the real + // authMiddleware exemption added for this route. + database.Seed(app.pool.Replicas[0], feedForYouFixtures()) + + var response struct { + Data []dbv1.Track + } + encodedId := trashid.MustEncodeHashID(1) + path := "/v1/users/" + encodedId + "/feed/for-you?user_id=" + encodedId + status, body := testGet(t, app, path, &response) + require.Equal(t, 200, status, string(body)) +} + func TestV1FeedForYou_ExcludesAlreadySavedTracks(t *testing.T) { app := emptyTestApp(t) app.skipAuthCheck = true