mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 06:35:29 +00:00
fix(auth): align WebSocket token_version checks with HTTP (#1744)
* test(auth): cover legacy no-tv JWT rejection on WebSocket upgrades * fix(auth): treat missing tv claim as token_version 1 on WebSocket upgrades
This commit is contained in:
@@ -351,6 +351,35 @@ describe('WebSocket upgrade - exec auth enforcement', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('rejects legacy no-tv admin JWT after token_version bump (401)', async () => {
|
||||||
|
// A legacy token without a tv claim is treated as version 1; once the
|
||||||
|
// account version is bumped it must be rejected on /ws like on HTTP.
|
||||||
|
const { DatabaseService } = await import('../services/DatabaseService');
|
||||||
|
const bcrypt = await import('bcrypt');
|
||||||
|
const db = DatabaseService.getInstance();
|
||||||
|
const username = `legacy-tv-admin-${Date.now()}`;
|
||||||
|
const id = db.addUser({
|
||||||
|
username,
|
||||||
|
password_hash: await bcrypt.hash('password123', 1),
|
||||||
|
role: 'admin',
|
||||||
|
});
|
||||||
|
db.bumpTokenVersion(id);
|
||||||
|
|
||||||
|
const legacyNoTv = jwt.sign(
|
||||||
|
{ username, role: 'admin' },
|
||||||
|
TEST_JWT_SECRET,
|
||||||
|
{ expiresIn: '1m' },
|
||||||
|
);
|
||||||
|
const ws = new WebSocket(getWsUrl(), { headers: { Cookie: `sencho_token=${legacyNoTv}` } });
|
||||||
|
const code = await new Promise<number>((resolve) => {
|
||||||
|
ws.on('unexpected-response', (_req, res) => resolve(res.statusCode ?? 0));
|
||||||
|
// A regression (upgrade accepted) must fail fast with 200, not hang.
|
||||||
|
ws.on('open', () => { ws.close(); resolve(200); });
|
||||||
|
ws.on('error', () => resolve(0));
|
||||||
|
});
|
||||||
|
expect(code).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
it('rejects WebSocket upgrade with node_proxy token (403)', async () => {
|
it('rejects WebSocket upgrade with node_proxy token (403)', async () => {
|
||||||
const token = jwt.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
const token = jwt.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
||||||
const ws = new WebSocket(getWsUrl(), { headers: { Authorization: `Bearer ${token}` } });
|
const ws = new WebSocket(getWsUrl(), { headers: { Authorization: `Bearer ${token}` } });
|
||||||
|
|||||||
@@ -85,6 +85,21 @@ describe('WebSocket upgrade - host console auth enforcement', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('rejects a legacy no-tv admin JWT after token_version bump (401)', async () => {
|
||||||
|
const { DatabaseService } = await import('../services/DatabaseService');
|
||||||
|
const db = DatabaseService.getInstance();
|
||||||
|
const username = `hc-legacy-tv-${Date.now()}`;
|
||||||
|
const id = db.addUser({
|
||||||
|
username,
|
||||||
|
password_hash: await bcrypt.hash('password123', 1),
|
||||||
|
role: 'admin',
|
||||||
|
});
|
||||||
|
db.bumpTokenVersion(id);
|
||||||
|
const legacyNoTv = jwt.sign({ username, role: 'admin' }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
||||||
|
const ws = new WebSocket(wsUrl(), { headers: { Cookie: `sencho_token=${legacyNoTv}` } });
|
||||||
|
expect(await expectRejected(ws)).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts a Community-tier admin', async () => {
|
it('accepts a Community-tier admin', async () => {
|
||||||
getTierSpy.mockReturnValueOnce('community');
|
getTierSpy.mockReturnValueOnce('community');
|
||||||
const ws = new WebSocket(wsUrl(), { headers: { Cookie: `sencho_token=${adminToken()}` } });
|
const ws = new WebSocket(wsUrl(), { headers: { Cookie: `sencho_token=${adminToken()}` } });
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ export function handleGenericWs(
|
|||||||
console.warn('[Exec] User account not found:', decoded.username);
|
console.warn('[Exec] User account not found:', decoded.username);
|
||||||
return reject(socket, 401, 'Unauthorized');
|
return reject(socket, 401, 'Unauthorized');
|
||||||
}
|
}
|
||||||
if (decoded.tv !== undefined && execUser.token_version !== decoded.tv) {
|
// Missing `tv` is a pre-migration legacy token at version 1, same default
|
||||||
|
// as `authMiddleware`; a bumped account version must reject it.
|
||||||
|
if (execUser.token_version !== (decoded.tv ?? 1)) {
|
||||||
console.warn('[Exec] Session invalidated (token version mismatch):', decoded.username);
|
console.warn('[Exec] Session invalidated (token version mismatch):', decoded.username);
|
||||||
return reject(socket, 401, 'Unauthorized');
|
return reject(socket, 401, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ export function attachUpgrade(
|
|||||||
if (!decoded.scope && decoded.username) {
|
if (!decoded.scope && decoded.username) {
|
||||||
const dbUser = DatabaseService.getInstance().getUserByUsername(decoded.username);
|
const dbUser = DatabaseService.getInstance().getUserByUsername(decoded.username);
|
||||||
if (!dbUser) return reject(socket, 401, 'Unauthorized');
|
if (!dbUser) return reject(socket, 401, 'Unauthorized');
|
||||||
if (decoded.tv !== undefined && dbUser.token_version !== decoded.tv) {
|
// Missing `tv` is a pre-migration legacy token at version 1, same default
|
||||||
|
// as `authMiddleware`; a bumped account version must reject it.
|
||||||
|
if (dbUser.token_version !== (decoded.tv ?? 1)) {
|
||||||
console.log('[Auth] WS session rejected: token version mismatch for:', decoded.username);
|
console.log('[Auth] WS session rejected: token version mismatch for:', decoded.username);
|
||||||
return reject(socket, 401, 'Unauthorized');
|
return reject(socket, 401, 'Unauthorized');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user