navidrome/ui/src
Deluan Quintão edffca24b1
fix(lastfm): require signed state token on link callback (#5521)
* fix(lastfm): require signed state token on link callback

The Last.fm OAuth callback at /api/lastfm/link/callback trusted a raw
\`uid\` query parameter and wrote the resulting Last.fm session key under
that user with no ownership check. Any authenticated user who learned a
victim's internal user ID (e.g. from playlist ownerId) could redirect the
victim's scrobbles to an attacker-controlled Last.fm account by calling
the callback directly with the victim's uid and a Last.fm token obtained
for their own account.

The callback cannot use the regular auth middleware because it is reached
via a browser redirect from Last.fm, which cannot carry a JWT header.
Instead, GET /api/lastfm/link (authenticated) now also returns a short-
lived (5 min) HMAC-signed link token bound to the requesting user, with a
dedicated "lastfm-link" scope claim. The callback verifies the signature,
scope and expiry before deriving the user ID from the token; the \`uid\`
query value is no longer trusted as a user identifier. The UI fetches
this token at link-flow start and passes it in place of the raw user ID.

Reuses the existing HS256 secret via auth.EncodeToken/DecodeAndVerifyToken
so no new key management is introduced.

* fix(ui): keep Last.fm popup tied to user gesture for Safari

Opening the Last.fm OAuth tab after an awaited fetch causes the popup to
be blocked on Safari and on Firefox with strict popup blocking enabled,
because the browser's transient-activation window has already elapsed by
the time window.open is reached. Linking became impossible on those
browsers in the previous commit.

Move the click handler up to the parent component and open a placeholder
about:blank tab synchronously from the click; the linkToken fetch then
runs in parallel and we redirect the existing tab to Last.fm's auth URL
once it resolves. The user gesture stays attached to the window.open
call, so popup blockers no longer fire.

The polling/progress UI is unchanged; it now receives the openedTab ref
from the parent instead of owning it.

* fix(lastfm): require exp claim on link tokens

jwtauth.VerifyToken treats a JWT without an exp claim as non-expiring, so
verifyLinkToken used to delegate expiry handling entirely. A future
regression in createLinkToken that dropped the exp field would silently
turn link tokens into permanent bearer credentials.

Assert presence of an exp claim explicitly and add a regression test
covering the missing-exp case. Also tightens the wrong-scope test to use
a freshly-minted token with all claims present except the scope, instead
of relying on auth.CreatePublicToken which happens to also be missing
exp.

* style(lastfm): simplify comments in link token code

Trim doc comments on createLinkToken/verifyLinkToken/callback/startLink
to the load-bearing lines: keep the non-obvious 'jwtauth treats missing
exp as non-expiring' note and the popup-blocker hint, drop the rest
since the function names already describe behavior.

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(lastfm): address review feedback on link token PR

- Wrap openInNewTab in a try/catch in startLink: openInNewTab calls
  win.focus() unconditionally, so if the browser blocks the popup
  (window.open returns null) it throws a TypeError synchronously,
  before the catch() on the link-token fetch is attached. The throw
  used to escape the click handler, leaving the UI without a
  notification. Now the failure is surfaced as lastfmLinkFailure and
  the toggle stays usable.
- Rename the link-token "subject" rejection message to "user ID" since
  the claim is uid, not the JWT sub field.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-23 12:16:15 -03:00
..
actions feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448) 2026-05-01 15:27:32 -04:00
album fix(ui): show album tile actions on keyboard focus (#5434) 2026-04-28 22:48:59 -04:00
artist feat(ui): add Not Starred filter option (#5362) 2026-04-27 18:21:32 -04:00
audioplayer fix(ui): report playback when restarting current track via prev 2026-05-22 22:23:36 -03:00
common fix(ui): refine image filters for playing and paused states in SquiddiesGlass 2026-04-08 13:13:56 -04:00
dataProvider feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448) 2026-05-01 15:27:32 -04:00
dialogs feat(ui): add download config toml link, disable copy when clipboard not available (#5035) 2026-02-12 10:54:04 -05:00
i18n fix(ui): update Russian translations and add missing gain keys (#5329) 2026-04-12 13:17:46 -04:00
icons build(ui): migrate from CRA/Jest to Vite/Vitest (#3311) 2024-09-28 11:54:36 -04:00
layout feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448) 2026-05-01 15:27:32 -04:00
library feat(ui): conditionally display 'path' field in LibraryList for desktop view 2026-03-01 10:50:37 -05:00
missing fix(ui): export all missing files instead of first 1000 2025-12-16 06:43:02 -05:00
personal fix(lastfm): require signed state token on link callback (#5521) 2026-05-23 12:16:15 -03:00
player build(ui): migrate from CRA/Jest to Vite/Vitest (#3311) 2024-09-28 11:54:36 -04:00
playlist fix(artwork): address WebP performance regression on low-power hardware (#5286) 2026-04-04 15:17:01 -04:00
plugin fix(ui): add Rescan button to plugin list empty state (#5471) 2026-05-05 18:49:24 -04:00
radio fix(artwork): address WebP performance regression on low-power hardware (#5286) 2026-04-04 15:17:01 -04:00
reducers feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448) 2026-05-01 15:27:32 -04:00
share fix(ui): allow scrolling in shareplayer queue by adding delay #4748 2025-11-29 12:54:46 -05:00
song feat(ui): add Not Starred filter option (#5362) 2026-04-27 18:21:32 -04:00
store feat: Multi-library support (#4181) 2025-07-18 18:41:12 -04:00
subsonic feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448) 2026-05-01 15:27:32 -04:00
themes fix(ui): updated the AMusic theme to use the correct text colour for primary confirmation buttons (#5509) 2026-05-19 11:01:41 -03:00
transcode fix(ui): improve browser codec detection and limit Safari transcoding to mp3 (#5171) 2026-03-12 08:21:49 -04:00
transcoding fix(ui): minor icon inconsistencies and "no missing files" translation (#3837) 2025-03-16 19:39:19 -04:00
user feat: Multi-library support (#4181) 2025-07-18 18:41:12 -04:00
utils fix(ui): use div for fragment, check lastfm url for artist page (#4980) 2026-02-04 17:34:26 -05:00
App.jsx feat(ui): integrate transcode decision into web player (#5155) 2026-03-09 11:06:31 -04:00
authProvider.js feat(server): add ExtAuth logout URL configuration (#5074) 2026-02-23 20:28:38 -05:00
config.js feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448) 2026-05-01 15:27:32 -04:00
consts.js fix(artwork): address WebP performance regression on low-power hardware (#5286) 2026-04-04 15:17:01 -04:00
eventStream.js fix(ui): keep the NowPlayingPanel badge in sync. 2025-06-29 11:35:10 -04:00
eventStream.test.js chore(deps): update all dependencies (#4618) 2025-10-25 17:05:16 -04:00
hotkeys.js Change "Go to current song" hotkey. 2022-12-18 20:58:01 -05:00
index.css chore(deps): bump JS dependencies 2025-03-07 19:47:08 -05:00
index.jsx fix(ui): service worker does not load new version of ui (#3402) 2024-10-16 21:39:14 -04:00
routes.jsx build(ui): migrate from CRA/Jest to Vite/Vitest (#3311) 2024-09-28 11:54:36 -04:00
setupTests.js Use the default import path for jest-dom. 2024-01-17 17:07:43 -05:00
sw.js fix(ui): service worker crashing on precacheAndRoute (#3528) 2024-12-08 17:43:34 -05:00
useChangeThemeColor.jsx build(ui): migrate from CRA/Jest to Vite/Vitest (#3311) 2024-09-28 11:54:36 -04:00
vite-env.d.ts build(ui): migrate from CRA/Jest to Vite/Vitest (#3311) 2024-09-28 11:54:36 -04:00