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
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import type { Request } from 'express'
|
||||
|
||||
const users = vi.hoisted(() => ({
|
||||
list: [] as { id: string; username: string; role: string }[],
|
||||
}))
|
||||
|
||||
vi.mock('../logger.js', () => ({
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('../users.js', () => ({
|
||||
hasUsers : () => users.list.length > 0,
|
||||
findByUsername: (name: string) => users.list.find(u => u.username.toLowerCase() === name.toLowerCase()),
|
||||
createUser : (username: string, _password: string, role: string) => {
|
||||
const user = { id: `id-${username}`, username, role }
|
||||
users.list.push(user)
|
||||
return user
|
||||
},
|
||||
}))
|
||||
|
||||
const ENV_KEYS = ['PROXY_AUTH_HEADER', 'PROXY_AUTH_TRUSTED_PROXIES', 'PROXY_AUTH_AUTO_CREATE', 'PROXY_AUTH_LOGOUT_URL']
|
||||
|
||||
async function load(env: Record<string, string>) {
|
||||
for (const key of ENV_KEYS) delete process.env[key]
|
||||
Object.assign(process.env, env)
|
||||
vi.resetModules()
|
||||
return import('./proxy-auth.js')
|
||||
}
|
||||
|
||||
function request(remoteAddress: string, headers: Record<string, string> = {}): Request {
|
||||
return { socket: { remoteAddress }, headers } as unknown as Request
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
users.list = [{ id: 'id-alice', username: 'Alice', role: 'admin' }]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) delete process.env[key]
|
||||
})
|
||||
|
||||
describe('adresses de confiance', () => {
|
||||
it('reconnaît une plage IPv4, une adresse exacte et une IPv4 vue par un socket IPv6', async () => {
|
||||
const { parseTrustedProxies, isTrustedAddress } = await load({})
|
||||
const trusted = parseTrustedProxies('172.18.0.0/16, 10.0.0.5, fd00::1')
|
||||
|
||||
expect(isTrustedAddress('172.18.3.4', trusted)).toBe(true)
|
||||
expect(isTrustedAddress('::ffff:172.18.0.2', trusted)).toBe(true)
|
||||
expect(isTrustedAddress('10.0.0.5', trusted)).toBe(true)
|
||||
expect(isTrustedAddress('FD00::1', trusted)).toBe(true)
|
||||
|
||||
expect(isTrustedAddress('172.19.0.1', trusted)).toBe(false)
|
||||
expect(isTrustedAddress('10.0.0.6', trusted)).toBe(false)
|
||||
expect(isTrustedAddress(undefined, trusted)).toBe(false)
|
||||
})
|
||||
|
||||
it('ignore les entrées invalides', async () => {
|
||||
const { parseTrustedProxies } = await load({})
|
||||
expect(parseTrustedProxies('pas-une-ip, 10.0.0.0/40, fd00::/64')).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proxyAuthUser', () => {
|
||||
const enabled = { PROXY_AUTH_HEADER: 'X-User', PROXY_AUTH_TRUSTED_PROXIES: '172.18.0.0/16' }
|
||||
|
||||
it('est désactivé sans adresse de confiance, même si l\'en-tête est défini', async () => {
|
||||
const mod = await load({ PROXY_AUTH_HEADER: 'X-User' })
|
||||
expect(mod.PROXY_AUTH_ENABLED).toBe(false)
|
||||
expect(mod.proxyAuthUser(request('172.18.0.2', { 'x-user': 'alice' }))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('renvoie le compte désigné par un proxy de confiance, sans tenir compte de la casse', async () => {
|
||||
const mod = await load(enabled)
|
||||
expect(mod.proxyAuthUser(request('172.18.0.2', { 'x-user': 'alice' }))?.id).toBe('id-alice')
|
||||
})
|
||||
|
||||
it('ignore l\'en-tête venant d\'une adresse non déclarée', async () => {
|
||||
const mod = await load(enabled)
|
||||
expect(mod.proxyAuthUser(request('192.168.1.50', { 'x-user': 'alice' }))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('ne fait rien sans en-tête', async () => {
|
||||
const mod = await load(enabled)
|
||||
expect(mod.proxyAuthUser(request('172.18.0.2'))).toBeUndefined()
|
||||
})
|
||||
|
||||
it('ne crée pas de compte inconnu par défaut', async () => {
|
||||
const mod = await load(enabled)
|
||||
expect(mod.proxyAuthUser(request('172.18.0.2', { 'x-user': 'bob' }))).toBeUndefined()
|
||||
expect(users.list).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('crée un compte utilisateur avec PROXY_AUTH_AUTO_CREATE', async () => {
|
||||
const mod = await load({ ...enabled, PROXY_AUTH_AUTO_CREATE: 'true' })
|
||||
const user = mod.proxyAuthUser(request('172.18.0.2', { 'x-user': 'bob' }))
|
||||
expect(user).toMatchObject({ username: 'bob', role: 'user' })
|
||||
})
|
||||
|
||||
it('laisse l\'assistant créer le premier compte', async () => {
|
||||
users.list = []
|
||||
const mod = await load({ ...enabled, PROXY_AUTH_AUTO_CREATE: 'true' })
|
||||
expect(mod.proxyAuthUser(request('172.18.0.2', { 'x-user': 'bob' }))).toBeUndefined()
|
||||
expect(users.list).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('n\'expose l\'adresse de déconnexion que si l\'authentification par proxy est active', async () => {
|
||||
expect((await load({ PROXY_AUTH_LOGOUT_URL: '/oauth2/sign_out' })).PROXY_AUTH_LOGOUT_URL).toBe('')
|
||||
expect((await load({ ...enabled, PROXY_AUTH_LOGOUT_URL: '/oauth2/sign_out' })).PROXY_AUTH_LOGOUT_URL).toBe('/oauth2/sign_out')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user