navidrome/ui/src/authProvider.test.js
Deluan Quintão fb45ad7b9c
fix(auth): ExtAuth logout redirect on unauthenticated loads, and warning spam from untrusted sources (#6176)
* fix(ui): only redirect to ExtAuth logout URL for proxy-authenticated sessions

react-admin calls authProvider.logout() when the boot-time checkAuth fails
and after a 401, not only when the user clicks Logout. With
ExtAuth.LogoutURL set, every unauthenticated page load (e.g. direct LAN
access that bypasses the auth proxy) was sent to the IdP sign-out page and
the login form was never shown.

Redirect only when the page was authenticated by the reverse proxy
(config.auth is present). Other sessions fall back to the login form.

Fixes #6175

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

* fix(server): only warn about untrusted ExtAuth sources when the header is sent

UsernameFromExtAuthHeader checked the source IP before looking for the user
header, so every request from an IP outside ExtAuth.TrustedSources logged a
warning, even when it carried no header at all. With direct LAN access
alongside a forward-auth proxy, a single polling client produced a constant
stream of warnings (twice per Subsonic request, since the middleware chain
resolves the username in both checkRequiredParameters and authenticate).

Look for the header first and warn only when an untrusted source actually
sends it, which is the case worth seeing: a misconfigured proxy or a spoof
attempt.

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-09-19 17:08:49 -04:00

47 lines
1.5 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import config from './config'
import authProvider from './authProvider'
vi.mock('./config', () => ({ default: {} }))
describe('authProvider.logout', () => {
const logoutURL = 'https://auth.example.com/signout'
beforeEach(() => {
vi.stubGlobal('location', { href: '' })
localStorage.setItem('is-authenticated', 'true')
localStorage.setItem('token', 'abc')
})
afterEach(() => {
vi.unstubAllGlobals()
localStorage.clear()
delete config.extAuthLogoutURL
delete config.auth
})
it('clears the stored session', async () => {
await authProvider.logout()
expect(localStorage.getItem('is-authenticated')).toBeNull()
expect(localStorage.getItem('token')).toBeNull()
})
it('does not redirect when no logout URL is configured', async () => {
config.auth = { id: '1' }
await expect(authProvider.logout()).resolves.toBeUndefined()
expect(window.location.href).toBe('')
})
it('redirects to the logout URL when the page was authenticated by the proxy', async () => {
config.extAuthLogoutURL = logoutURL
config.auth = { id: '1' }
await expect(authProvider.logout()).resolves.toBe(false)
expect(window.location.href).toBe(logoutURL)
})
it('does not redirect when the page was not authenticated by the proxy', async () => {
config.extAuthLogoutURL = logoutURL
await expect(authProvider.logout()).resolves.toBeUndefined()
expect(window.location.href).toBe('')
})
})