mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-05 00:18:00 +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 () => {
|
||||
const token = jwt.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
||||
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 () => {
|
||||
getTierSpy.mockReturnValueOnce('community');
|
||||
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);
|
||||
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);
|
||||
return reject(socket, 401, 'Unauthorized');
|
||||
}
|
||||
|
||||
@@ -201,7 +201,9 @@ export function attachUpgrade(
|
||||
if (!decoded.scope && decoded.username) {
|
||||
const dbUser = DatabaseService.getInstance().getUserByUsername(decoded.username);
|
||||
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);
|
||||
return reject(socket, 401, 'Unauthorized');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user