Skip to content

Commit e8194b7

Browse files
dylanjeffersclaude
andauthored
fix(auth): allow unauthenticated user_id on /feed/for-you (#804)
## Summary The For You handler (`v1_users_feed_for_you.go`) treats: - **path `:userId`** as the personalization target - **query `user_id`** as a viewer hint, used only to decorate response fields like `has_current_user_reposted` / `has_current_user_saved` It already calls `tryGetAuthedWallet` (optional), so the handler is fine with an unauthenticated request — the path `:userId` controls the personalization SQL, and the access-gated track filter falls through to "ungated only" when `authedWallet` is empty. But the global `authMiddleware` returns 403 whenever `user_id` is set and the request isn't signed: ```go // auth_middleware.go:351 if myId != 0 && !pkceAuthed && !app.isAuthorizedRequest(c.Context(), myId, wallet) { return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("You are not authorized to make this request authedWallet=%s myId=%d", wallet, myId)) } ``` …so the call dies before reaching the handler. Symptom on the web RC: For You tab silently empty / 403 in network tab. ## Fix Exempt the `/feed/for-you` route from that strict check via `strings.HasSuffix(c.Path(), "/feed/for-you")`. The `myId` is still resolved (so viewer-relative fields populate when a `user_id` is supplied) — it's just not gated behind a wallet match. ## What stays the same - Path `:userId` still controls **what** content is returned — caller can't impersonate a different user for personalization. - Access-gated tracks still filtered out when `authedWallet` is empty (handler's existing `t.access_authorities` predicate). - All other routes still get the strict 403. ## Test plan - ✅ New test `TestV1FeedForYou_UnauthenticatedViewerIdAllowed` exercises the exemption with `skipAuthCheck` OFF (so the real auth path runs). Passes locally against the test DB. - ✅ Existing For You tests still pass (`TestV1FeedForYou_Basic`, `TestV1FeedForYou_RequiresValidUserId`, `TestV1FeedForYou_ExcludesAlreadySavedTracks`, etc.). - ✅ `go build ./api/...` / `go vet ./api/...` clean. - After merge: web RC `/v1/users/{id}/feed/for-you?user_id={id}` should return 200 instead of 403 — verifiable directly with curl. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 386ac75 commit e8194b7

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

api/auth_middleware.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,16 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
347347
return fiber.NewError(fiber.StatusUnauthorized, "Invalid or expired access token")
348348
}
349349

350-
// Not authorized to act on behalf of myId
351-
if myId != 0 && !pkceAuthed && !app.isAuthorizedRequest(c.Context(), myId, wallet) {
350+
// Not authorized to act on behalf of myId.
351+
//
352+
// Exception: /users/:userId/feed/for-you accepts user_id as a viewer hint
353+
// used only for response decoration (has_current_user_reposted etc.); the
354+
// path :userId — not user_id — controls what gets personalized. Treat the
355+
// query user_id as advisory rather than authoritative on this route so
356+
// the endpoint can be called like the other public read endpoints.
357+
allowUnauthenticatedViewerId := strings.HasSuffix(c.Path(), "/feed/for-you")
358+
359+
if myId != 0 && !pkceAuthed && !allowUnauthenticatedViewerId && !app.isAuthorizedRequest(c.Context(), myId, wallet) {
352360
return fiber.NewError(
353361
fiber.StatusForbidden,
354362
fmt.Sprintf(

api/v1_users_feed_for_you_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,25 @@ func TestV1FeedForYou_RequiresValidUserId(t *testing.T) {
148148
assert.Equal(t, 400, status)
149149
}
150150

151+
// /feed/for-you is exempt from authMiddleware's "if user_id is set, wallet
152+
// must match" 403 — the query user_id is a viewer hint, not an authorization
153+
// claim. This test pins that exemption: with skipAuthCheck OFF (so the real
154+
// auth path runs) and no signature headers, the call still returns 200.
155+
func TestV1FeedForYou_UnauthenticatedViewerIdAllowed(t *testing.T) {
156+
app := emptyTestApp(t)
157+
// Deliberately NOT setting app.skipAuthCheck — exercise the real
158+
// authMiddleware exemption added for this route.
159+
database.Seed(app.pool.Replicas[0], feedForYouFixtures())
160+
161+
var response struct {
162+
Data []dbv1.Track
163+
}
164+
encodedId := trashid.MustEncodeHashID(1)
165+
path := "/v1/users/" + encodedId + "/feed/for-you?user_id=" + encodedId
166+
status, body := testGet(t, app, path, &response)
167+
require.Equal(t, 200, status, string(body))
168+
}
169+
151170
func TestV1FeedForYou_ExcludesAlreadySavedTracks(t *testing.T) {
152171
app := emptyTestApp(t)
153172
app.skipAuthCheck = true

0 commit comments

Comments
 (0)