mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-20 00:07:12 +02:00
* 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>
114 lines
3.3 KiB
JavaScript
114 lines
3.3 KiB
JavaScript
import { jwtDecode } from 'jwt-decode'
|
|
import { baseUrl } from './utils'
|
|
import config from './config'
|
|
|
|
// config sent from server may contain authentication info, for example when the user is authenticated
|
|
// by a reverse proxy request header
|
|
if (config.auth) {
|
|
try {
|
|
storeAuthenticationInfo(config.auth)
|
|
} catch (e) {
|
|
// eslint-disable-next-line no-console
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
function storeAuthenticationInfo(authInfo) {
|
|
authInfo.token && localStorage.setItem('token', authInfo.token)
|
|
localStorage.setItem('userId', authInfo.id)
|
|
localStorage.setItem('name', authInfo.name)
|
|
localStorage.setItem('username', authInfo.username)
|
|
authInfo.avatar && localStorage.setItem('avatar', authInfo.avatar)
|
|
localStorage.setItem('role', authInfo.isAdmin ? 'admin' : 'regular')
|
|
localStorage.setItem('subsonic-salt', authInfo.subsonicSalt)
|
|
localStorage.setItem('subsonic-token', authInfo.subsonicToken)
|
|
localStorage.setItem('is-authenticated', 'true')
|
|
}
|
|
|
|
const authProvider = {
|
|
login: ({ username, password }) => {
|
|
let url = baseUrl('/auth/login')
|
|
if (config.firstTime) {
|
|
url = baseUrl('/auth/createAdmin')
|
|
}
|
|
const request = new Request(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password }),
|
|
headers: new Headers({ 'Content-Type': 'application/json' }),
|
|
})
|
|
return fetch(request)
|
|
.then((response) => {
|
|
if (response.status < 200 || response.status >= 300) {
|
|
throw new Error(response.statusText)
|
|
}
|
|
return response.json()
|
|
})
|
|
.then((response) => {
|
|
jwtDecode(response.token) // Validate token
|
|
storeAuthenticationInfo(response)
|
|
// Avoid "going to create admin" dialog after logout/login without a refresh
|
|
config.firstTime = false
|
|
return response
|
|
})
|
|
.catch((error) => {
|
|
if (
|
|
error.message === 'Failed to fetch' ||
|
|
error.stack === 'TypeError: Failed to fetch'
|
|
) {
|
|
throw new Error('errors.network_error')
|
|
}
|
|
|
|
throw new Error(error)
|
|
})
|
|
},
|
|
|
|
logout: () => {
|
|
removeItems()
|
|
// Only proxy-authenticated sessions go to the IdP; others (e.g. direct LAN access) get the login form
|
|
if (config.extAuthLogoutURL && config.auth) {
|
|
window.location.href = config.extAuthLogoutURL
|
|
return Promise.resolve(false)
|
|
}
|
|
return Promise.resolve()
|
|
},
|
|
|
|
checkAuth: () =>
|
|
localStorage.getItem('is-authenticated')
|
|
? Promise.resolve()
|
|
: Promise.reject(),
|
|
|
|
checkError: ({ status }) => {
|
|
if (status === 401) {
|
|
removeItems()
|
|
return Promise.reject()
|
|
}
|
|
return Promise.resolve()
|
|
},
|
|
|
|
getPermissions: () => {
|
|
const role = localStorage.getItem('role')
|
|
return role ? Promise.resolve(role) : Promise.reject()
|
|
},
|
|
|
|
getIdentity: () => {
|
|
return Promise.resolve({
|
|
id: localStorage.getItem('username'),
|
|
fullName: localStorage.getItem('name'),
|
|
avatar: localStorage.getItem('avatar'),
|
|
})
|
|
},
|
|
}
|
|
|
|
const removeItems = () => {
|
|
localStorage.removeItem('token')
|
|
localStorage.removeItem('userId')
|
|
localStorage.removeItem('name')
|
|
localStorage.removeItem('username')
|
|
localStorage.removeItem('avatar')
|
|
localStorage.removeItem('role')
|
|
localStorage.removeItem('subsonic-salt')
|
|
localStorage.removeItem('subsonic-token')
|
|
localStorage.removeItem('is-authenticated')
|
|
}
|
|
|
|
export default authProvider
|