feat(auth): authentification par proxy (oauth2-proxy, Authelia…)
Derrière un proxy d'authentification, FanKarr demandait une seconde connexion. Avec PROXY_AUTH_HEADER, la session est ouverte à partir du nom d'utilisateur transmis par le proxy. - server/lib/proxy-auth.ts : lecture de l'en-tête, uniquement pour les connexions venant de PROXY_AUTH_TRUSTED_PROXIES (adresse du socket, pas X-Forwarded-For) ; fonction désactivée si cette liste est vide - compte rapproché par nom (sans casse) ; création d'un compte utilisateur sans mot de passe avec PROXY_AUTH_AUTO_CREATE ; premier compte toujours créé par l'assistant - server/auth.ts : middleware proxyAuth avant les routes, qui ouvre ou remplace la session ; /api/auth/status expose proxyAuth et logoutUrl - déconnexion : redirection vers PROXY_AUTH_LOGOUT_URL si défini - profil et gestion des utilisateurs : libellés pour les comptes sans mot de passe qui ne viennent pas de Jellyfin - tests vitest du module ; fichiers de test exclus de la compilation serveur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
804436ff70
commit
e13d43d186
13 files changed
+349
-18
No files matched your search
+33
-5
@@ -15,6 +15,7 @@ import { readSettings, type Settings } from './settings.js'
|
||||
import { startOnboarding } from './onboarding.js'
|
||||
import { isJellyfinConfigured, authenticateJellyfinUser, getJellyfinServerId } from './lib/jellyfin.js'
|
||||
import { loginRetryAfter, recordLoginFailure, clearLoginFailures } from './lib/login-throttle.js'
|
||||
import { proxyAuthUser, PROXY_AUTH_ENABLED, PROXY_AUTH_LOGOUT_URL } from './lib/proxy-auth.js'
|
||||
|
||||
// ── Typage Express étendu ─────────────────────────────────────
|
||||
declare global {
|
||||
@@ -39,13 +40,39 @@ function signToken(user: User): string {
|
||||
: jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY as any })
|
||||
}
|
||||
|
||||
function setCookieAndRespond(res: Response, user: User): void {
|
||||
function setSessionCookie(res: Response, user: User): string {
|
||||
user.lastLoginAt = markLogin(user.id) ?? user.lastLoginAt
|
||||
const token = signToken(user)
|
||||
res.cookie('fankarr_token', token, { httpOnly: true, sameSite: 'lax', path: COOKIE_PATH })
|
||||
return token
|
||||
}
|
||||
|
||||
function setCookieAndRespond(res: Response, user: User): void {
|
||||
setSessionCookie(res, user)
|
||||
res.json({ success: true, user: safeUser(user) })
|
||||
}
|
||||
|
||||
// Identifiant du compte de la session, ou undefined si le cookie est absent ou invalide
|
||||
function sessionUserId(req: Request): string | undefined {
|
||||
const token = req.cookies?.fankarr_token
|
||||
if (!token) return undefined
|
||||
try { return (jwt.verify(token, JWT_SECRET) as any).id } catch { return undefined }
|
||||
}
|
||||
|
||||
// ── Authentification par proxy ────────────────────────────────
|
||||
// Placé avant les routes : quand le proxy désigne un compte, la session FanKarr
|
||||
// est ouverte (ou remplacée si elle appartient à un autre compte), et la suite
|
||||
// du traitement voit un cookie valide comme après une connexion classique.
|
||||
export function proxyAuth(req: Request, res: Response, next: NextFunction): void {
|
||||
const user = proxyAuthUser(req)
|
||||
if (user && sessionUserId(req) !== user.id) {
|
||||
const token = setSessionCookie(res, user)
|
||||
req.cookies = { ...req.cookies, fankarr_token: token }
|
||||
logger.info('auth', `Connexion de « ${user.username} » via le proxy d'authentification (rôle : ${user.role})`)
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
// ── Routes ────────────────────────────────────────────────────
|
||||
|
||||
function jellyfinLoginEnabled(s: Settings): boolean {
|
||||
@@ -57,21 +84,22 @@ export function authStatus(req: Request, res: Response): void {
|
||||
const settings = readSettings()
|
||||
const setup = hasUsers()
|
||||
const jellyfinLogin = jellyfinLoginEnabled(settings)
|
||||
const proxy = { proxyAuth: PROXY_AUTH_ENABLED, logoutUrl: PROXY_AUTH_LOGOUT_URL || null }
|
||||
const token = req.cookies?.fankarr_token
|
||||
|
||||
if (!token) { res.json({ setup, loggedIn: false, jellyfinLogin }); return }
|
||||
if (!token) { res.json({ setup, loggedIn: false, jellyfinLogin, ...proxy }); return }
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET) as any
|
||||
const user = findById(payload.id)
|
||||
if (!user) { res.json({ setup, loggedIn: false, jellyfinLogin }); return }
|
||||
if (!user) { res.json({ setup, loggedIn: false, jellyfinLogin, ...proxy }); return }
|
||||
res.json({
|
||||
setup, loggedIn: true, jellyfinLogin, role: user.role, username: user.username, userId: user.id,
|
||||
setup, loggedIn: true, jellyfinLogin, ...proxy, role: user.role, username: user.username, userId: user.id,
|
||||
onboardingDone: !!settings.onboardingCompletedAt,
|
||||
tourSeen : !!user.tourSeenAt,
|
||||
})
|
||||
} catch {
|
||||
res.json({ setup, loggedIn: false, jellyfinLogin })
|
||||
res.json({ setup, loggedIn: false, jellyfinLogin, ...proxy })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user