fix(auth): persist login and skip redundant login page
This commit is contained in:
@@ -103,7 +103,11 @@ async function route(req, res) {
|
||||
clearLoginFailures(req);
|
||||
return sendJsonWithHeaders(res, 200, loginHeaders(config), { ok: true });
|
||||
}
|
||||
if (url.pathname === '/login' || url.pathname === '/login.html' || url.pathname === '/styles.css' || url.pathname === '/login.js') return serveStatic(req, res);
|
||||
if (url.pathname === '/login' || url.pathname === '/login.html') {
|
||||
if (isAuthenticated(req, config)) return redirect(res, '/');
|
||||
return serveStatic(req, res);
|
||||
}
|
||||
if (url.pathname === '/styles.css' || url.pathname === '/login.js') return serveStatic(req, res);
|
||||
if (!isAuthenticated(req, config)) {
|
||||
if (url.pathname.startsWith('/api/')) return sendJson(res, 401, { ok: false, error: 'Authentication required.' });
|
||||
return redirect(res, '/login');
|
||||
|
||||
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'n
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
const COOKIE_NAME = 'kanban_session';
|
||||
const SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
||||
const attempts = new Map();
|
||||
const LOGIN_WINDOW_MS = 5 * 60 * 1000;
|
||||
const MAX_LOGIN_FAILURES = 8;
|
||||
@@ -101,7 +102,7 @@ export function isAuthenticated(req, config) {
|
||||
export function loginHeaders(config) {
|
||||
const secure = config.authSecureCookie ? ' Secure;' : '';
|
||||
return {
|
||||
'set-cookie': `${COOKIE_NAME}=${encodeURIComponent(config.auth.token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000;${secure}`
|
||||
'set-cookie': `${COOKIE_NAME}=${encodeURIComponent(config.auth.token)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${SESSION_MAX_AGE_SECONDS};${secure}`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
67
test/auth.test.js
Normal file
67
test/auth.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
test('login persists authentication and skips the login page on later visits', async () => {
|
||||
const testDirectory = mkdtempSync(join(tmpdir(), 'local-kanban-auth-'));
|
||||
const token = 'test-only-auth-token';
|
||||
process.env.KANBAN_CONFIG = join(testDirectory, 'config.json');
|
||||
process.env.KANBAN_STATE = join(testDirectory, 'state.json');
|
||||
process.env.KANBAN_AUTH_TOKEN = token;
|
||||
process.env.KANBAN_AUTH_SECURE_COOKIE = '1';
|
||||
process.env.KANBAN_REQUIRE_HTTPS = '0';
|
||||
|
||||
const { createKanbanServer } = await import(`../src/server.js?auth-test=${Date.now()}`);
|
||||
const server = createKanbanServer();
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const { port } = server.address();
|
||||
const origin = `http://127.0.0.1:${port}`;
|
||||
|
||||
const initialLoginPage = await fetch(`${origin}/login`, { redirect: 'manual' });
|
||||
assert.equal(initialLoginPage.status, 200);
|
||||
|
||||
const rejectedLogin = await fetch(`${origin}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'wrong-token' })
|
||||
});
|
||||
assert.equal(rejectedLogin.status, 401);
|
||||
assert.equal(rejectedLogin.headers.get('set-cookie'), null);
|
||||
|
||||
const loginResponse = await fetch(`${origin}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ token })
|
||||
});
|
||||
assert.equal(loginResponse.status, 200);
|
||||
|
||||
const setCookie = loginResponse.headers.get('set-cookie');
|
||||
assert.match(setCookie, /^kanban_session=[^;]+;/);
|
||||
const cookie = setCookie.split(';', 1)[0];
|
||||
|
||||
const laterLoginPage = await fetch(`${origin}/login`, {
|
||||
headers: { cookie },
|
||||
redirect: 'manual'
|
||||
});
|
||||
assert.equal(laterLoginPage.status, 302);
|
||||
assert.equal(laterLoginPage.headers.get('location'), '/');
|
||||
assert.match(setCookie, /Max-Age=31536000/);
|
||||
assert.match(setCookie, /HttpOnly/);
|
||||
assert.match(setCookie, /Secure/);
|
||||
|
||||
const logoutResponse = await fetch(`${origin}/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { cookie }
|
||||
});
|
||||
assert.equal(logoutResponse.status, 200);
|
||||
assert.match(logoutResponse.headers.get('set-cookie'), /^kanban_session=;/);
|
||||
assert.match(logoutResponse.headers.get('set-cookie'), /Max-Age=0/);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
rmSync(testDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user