fix(navidrome): let non-browser clients reach the native API #1072

Merged
k.renner merged 1 commits from fix/navidrome-native-api into master 2026-09-29 13:42:16 +00:00
Member

Problem

Koda (Garmin Connect IQ app, Forerunner 965) shows "Native login failed -400" after its first sync.

Traefik access log, 2026-09-29: every failing sync does GET /rest/getPlaylist.view (200) and, in the same second, POST /auth/login → 302, 0 bytes (the navidrome-gate redirect), then no stream requests:

13:18:13 POST /auth/login 302 0
13:18:35 POST /auth/login 302 0
13:24:11 POST /auth/login 302 0
13:26:20 POST /auth/login 302 0
13:34:10 POST /auth/login 302 0

Connect IQ error -400 is INVALID_HTTP_BODY_IN_NETWORK_RESPONSE ("Response body data is invalid for the request type", Toybox.Communications). Only /rest and /share bypass the gate today, so Navidrome's native login is unreachable for any client that can't follow the SSO redirect. (The first sync at 11:00 UTC made no /auth/login call, which is why it worked; what makes Koda switch to the native API is on the app side.)

Change

One IngressRoute (navidrome-native-api, priority 1000):

Host(`navidrome.renner.uno`) && (PathPrefix(`/auth`) || PathPrefix(`/api`)) && !HeaderRegexp(`Cookie`, `authentik_proxy_`)

→ navidrome:80 via the existing navidrome-strip-identity middleware.

  • Requests without an Authentik session cookie (watch, apps) reach Navidrome directly and authenticate with the Navidrome password (built-in login rate limit 5/20s), then JWT for /api. Same exposure class as /rest today.
  • Requests with the cookie (the web UI, cookie authentik_proxy_<id>, Domain=renner.uno, observed live) don't match and keep going through the gate, so SSO for the web UI is unchanged.
  • X-Authentik-Username is blanked on this route, so the ExtAuth header can't be spoofed.
  • /app (the UI itself) stays gated.

Matcher syntax checked against the Traefik v3 rules reference (cluster runs traefik v3.7.4). Entrypoint-default middlewares (security headers, rate limit) still apply on websecure.

Verification done

  • kubectl kustomize kubernetes/apps/navidrome builds; server-side dry-run of ingress.yaml accepts the IngressRoute.

After merge (I'll check)

  • curl -X POST https://navidrome.renner.uno/auth/login without a cookie → JSON from Navidrome (not a 302).
  • Same request with an authentik_proxy_ cookie → still handled by the gate.
  • Web UI still logs in via SSO; watch sync completes (Traefik log shows /auth/login 200 and stream calls).

🤖 Generated with Claude Code

## Problem Koda (Garmin Connect IQ app, Forerunner 965) shows **"Native login failed -400"** after its first sync. Traefik access log, 2026-09-29: every failing sync does `GET /rest/getPlaylist.view` (200) and, in the same second, `POST /auth/login` → **302, 0 bytes** (the navidrome-gate redirect), then no `stream` requests: ``` 13:18:13 POST /auth/login 302 0 13:18:35 POST /auth/login 302 0 13:24:11 POST /auth/login 302 0 13:26:20 POST /auth/login 302 0 13:34:10 POST /auth/login 302 0 ``` Connect IQ error -400 is `INVALID_HTTP_BODY_IN_NETWORK_RESPONSE` ("Response body data is invalid for the request type", [Toybox.Communications](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html)). Only `/rest` and `/share` bypass the gate today, so Navidrome's native login is unreachable for any client that can't follow the SSO redirect. (The first sync at 11:00 UTC made no `/auth/login` call, which is why it worked; what makes Koda switch to the native API is on the app side.) ## Change One `IngressRoute` (`navidrome-native-api`, priority 1000): ``` Host(`navidrome.renner.uno`) && (PathPrefix(`/auth`) || PathPrefix(`/api`)) && !HeaderRegexp(`Cookie`, `authentik_proxy_`) ``` → `navidrome:80` via the existing `navidrome-strip-identity` middleware. - **Requests without an Authentik session cookie** (watch, apps) reach Navidrome directly and authenticate with the Navidrome password (built-in login rate limit 5/20s), then JWT for `/api`. Same exposure class as `/rest` today. - **Requests with the cookie** (the web UI, cookie `authentik_proxy_<id>`, Domain=renner.uno, observed live) don't match and keep going through the gate, so SSO for the web UI is unchanged. - `X-Authentik-Username` is blanked on this route, so the ExtAuth header can't be spoofed. - `/app` (the UI itself) stays gated. Matcher syntax checked against the [Traefik v3 rules reference](https://doc.traefik.io/traefik/reference/routing-configuration/http/routing/rules-and-priority/) (cluster runs traefik v3.7.4). Entrypoint-default middlewares (security headers, rate limit) still apply on `websecure`. ## Verification done - `kubectl kustomize kubernetes/apps/navidrome` builds; server-side dry-run of `ingress.yaml` accepts the IngressRoute. ## After merge (I'll check) - `curl -X POST https://navidrome.renner.uno/auth/login` without a cookie → JSON from Navidrome (not a 302). - Same request with an `authentik_proxy_` cookie → still handled by the gate. - Web UI still logs in via SSO; watch sync completes (Traefik log shows `/auth/login` 200 and `stream` calls). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-09-29 13:41:51 +00:00
fix(navidrome): let non-browser clients reach the native API
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
1184a552fb
Koda (Garmin Connect IQ) calls POST /auth/login during sync. Only /rest
and /share bypassed the navidrome-gate, so the gate answered 302 with an
empty body and the watch failed with -400
INVALID_HTTP_BODY_IN_NETWORK_RESPONSE ("Native login failed -400").

An IngressRoute sends /auth and /api straight to Navidrome when the
request carries no authentik_proxy_ session cookie; requests with the
cookie (the web UI) keep going through the gate, so SSO is unchanged.
X-Authentik-Username is stripped on this route as on /rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k.renner merged commit 37f2bd0718 into master 2026-09-29 13:42:16 +00:00
Sign in to join this conversation.