Innebygd innlogging (AUTH_MODE=app) med neon-login og proxy-bypass
Valgfri app-auth ved siden av proxy-modus (default uendret). Signert sesjons-cookie (scrypt-passord, HMAC, ingen avhengigheter), gating av API + WebSocket, og en selvstendig neon-login (public/login.html) for uautentiserte. Trusted bypass: en proxy-identitet (X-Remote-User / Basic Auth) regnes som innlogget uten app-passord (TRUST_PROXY_HEADER, default på), så man kan bootstrappe: logg inn via NPM Basic Auth → sett app-passord i konto-modalen → skru av Basic Auth. Konto-UI for sett/endre passord + logg ut. AUTH_MODE leses per createServer-instans (testbart). 34 integrasjonstester (8 nye for app-auth), alle grønne. README/CLAUDE.md/setup-skript oppdatert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7287f33e47
commit
f6268440d9
9 changed files with 711 additions and 42 deletions
|
|
@ -402,3 +402,109 @@ test('cookie-auth: login setter cookie, ugyldig navn avvises', async () => {
|
|||
});
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
// ---------- App-modus innlogging (AUTH_MODE=app) ----------
|
||||
let appServer, appUrl, appDir;
|
||||
|
||||
function aReq(method, p, body, headers = {}) {
|
||||
return fetch(appUrl + p, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', ...headers },
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
function cookieFrom(res) {
|
||||
const sc = res.headers.get('set-cookie') || '';
|
||||
const m = sc.match(/netops_session=([^;]*)/);
|
||||
return m ? `netops_session=${m[1]}` : '';
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'netops-app-'));
|
||||
({ server: appServer } = createServer({ dataDir: appDir, authMode: 'app', sessionTtlDays: 7 }));
|
||||
await new Promise(r => appServer.listen(0, '127.0.0.1', r));
|
||||
appUrl = `http://127.0.0.1:${appServer.address().port}`;
|
||||
});
|
||||
after(() => {
|
||||
appServer.close();
|
||||
fs.rmSync(appDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('app-auth: uten innlogging gir 401 på API med login-flagg', async () => {
|
||||
const res = await aReq('GET', '/api/state');
|
||||
assert.equal(res.status, 401);
|
||||
assert.equal((await res.json()).login, true);
|
||||
});
|
||||
|
||||
test('app-auth: navigasjon uten innlogging serverer login-siden', async () => {
|
||||
const res = await aReq('GET', '/', undefined, { Accept: 'text/html' });
|
||||
assert.equal(res.status, 200);
|
||||
const html = await res.text();
|
||||
assert.match(html, /NetOps/);
|
||||
assert.match(html, /api\/auth\/login/); // login-skjemaet
|
||||
});
|
||||
|
||||
test('app-auth: proxy-header gir trusted bypass', async () => {
|
||||
const res = await aReq('GET', '/api/state', undefined, { 'X-Remote-User': 'jon' });
|
||||
assert.equal(res.status, 200);
|
||||
const data = await res.json();
|
||||
assert.equal(data.user, 'jon');
|
||||
assert.equal(data.auth.via, 'proxy');
|
||||
assert.equal(data.auth.hasPassword, false);
|
||||
});
|
||||
|
||||
test('app-auth: bootstrap — sett passord via bypass, logg så inn med passord', async () => {
|
||||
// Sett passord mens vi er innlogget via proxy
|
||||
let res = await aReq('POST', '/api/auth/password', { password: 'hemmelig123' }, { 'X-Remote-User': 'jon' });
|
||||
assert.equal(res.status, 200);
|
||||
assert.ok(cookieFrom(res), 'fikk sesjons-cookie ved passord-setting');
|
||||
|
||||
// Feil passord avvises
|
||||
res = await aReq('POST', '/api/auth/login', { user: 'jon', password: 'feil' });
|
||||
assert.equal(res.status, 401);
|
||||
|
||||
// Riktig passord gir sesjons-cookie (uten proxy-header)
|
||||
res = await aReq('POST', '/api/auth/login', { user: 'jon', password: 'hemmelig123' });
|
||||
assert.equal(res.status, 200);
|
||||
const cookie = cookieFrom(res);
|
||||
assert.ok(cookie);
|
||||
|
||||
// Sesjons-cookien gir tilgang uten proxy-header
|
||||
res = await aReq('GET', '/api/state', undefined, { Cookie: cookie });
|
||||
assert.equal(res.status, 200);
|
||||
const data = await res.json();
|
||||
assert.equal(data.user, 'jon');
|
||||
assert.equal(data.auth.via, 'session');
|
||||
assert.equal(data.auth.hasPassword, true);
|
||||
});
|
||||
|
||||
test('app-auth: for kort passord avvises', async () => {
|
||||
const res = await aReq('POST', '/api/auth/password', { password: 'kort' }, { 'X-Remote-User': 'kari' });
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
test('app-auth: logout tømmer cookien', async () => {
|
||||
const res = await aReq('POST', '/api/auth/logout');
|
||||
assert.match(res.headers.get('set-cookie') || '', /netops_session=;|Max-Age=0/);
|
||||
});
|
||||
|
||||
test('app-auth: TRUST_PROXY_HEADER=false ignorerer proxy-header', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'netops-app2-'));
|
||||
const { server } = createServer({ dataDir: dir, authMode: 'app', trustProxyHeader: false });
|
||||
await new Promise(r => server.listen(0, '127.0.0.1', r));
|
||||
const url = `http://127.0.0.1:${server.address().port}`;
|
||||
try {
|
||||
const res = await fetch(url + '/api/state', { headers: { 'X-Remote-User': 'jon' } });
|
||||
assert.equal(res.status, 401); // bypass av
|
||||
} finally {
|
||||
server.close();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('proxy-modus (default): ingen guard, alt åpent', async () => {
|
||||
// baseUrl-serveren kjører i proxy-modus — bekreft at /api/state er åpent uten cookie
|
||||
const res = await fetch(baseUrl + '/api/state', { headers: { 'X-Remote-User': 'whoever' } });
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal((await res.json()).auth.mode, 'proxy');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue