feat(worker): user-scoped box terminal token verification
This commit is contained in:
@@ -27,3 +27,28 @@ export const verifyTerminalToken = (
|
|||||||
timingSafeEqual(providedBuf, expectedBuf)
|
timingSafeEqual(providedBuf, expectedBuf)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// User-scoped variant authorizing a terminal connection to a user's box.
|
||||||
|
// Embeds a literal `box` segment so its four parts never collide with the
|
||||||
|
// three-part job token above. Format:
|
||||||
|
// `${expiresAtMs}.box.${username}.${hmacSha256Hex}`
|
||||||
|
export const verifyBoxTerminalToken = (
|
||||||
|
token: string,
|
||||||
|
username: string,
|
||||||
|
secret: string,
|
||||||
|
): boolean => {
|
||||||
|
if (!token || !secret) return false;
|
||||||
|
const parts = token.split('.');
|
||||||
|
if (parts.length !== 4 || parts[1] !== 'box') return false;
|
||||||
|
const [expRaw, , tokenUsername, provided] = parts;
|
||||||
|
if (tokenUsername !== username) return false;
|
||||||
|
const exp = Number.parseInt(expRaw ?? '', 10);
|
||||||
|
if (!Number.isFinite(exp) || Date.now() > exp) return false;
|
||||||
|
const expected = signature(`${expRaw}.box.${tokenUsername}`, secret);
|
||||||
|
const providedBuf = Buffer.from(provided ?? '', 'hex');
|
||||||
|
const expectedBuf = Buffer.from(expected, 'hex');
|
||||||
|
return (
|
||||||
|
providedBuf.length === expectedBuf.length &&
|
||||||
|
timingSafeEqual(providedBuf, expectedBuf)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { createHmac } from 'node:crypto';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { verifyBoxTerminalToken } from '../../src/terminal-token';
|
||||||
|
|
||||||
|
const mintBox = (username: string, expiresAt: number, secret: string) => {
|
||||||
|
const payload = `${expiresAt}.box.${username}`;
|
||||||
|
const sig = createHmac('sha256', secret).update(payload).digest('hex');
|
||||||
|
return `${payload}.${sig}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('verifyBoxTerminalToken', () => {
|
||||||
|
const secret = 'test-secret';
|
||||||
|
|
||||||
|
test('accepts a valid, unexpired, username-matched token', () => {
|
||||||
|
const token = mintBox('alice', Date.now() + 60_000, secret);
|
||||||
|
expect(verifyBoxTerminalToken(token, 'alice', secret)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an expired token', () => {
|
||||||
|
const token = mintBox('alice', Date.now() - 1, secret);
|
||||||
|
expect(verifyBoxTerminalToken(token, 'alice', secret)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a token minted for another username', () => {
|
||||||
|
const token = mintBox('alice', Date.now() + 60_000, secret);
|
||||||
|
expect(verifyBoxTerminalToken(token, 'bob', secret)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a token signed with a different secret', () => {
|
||||||
|
const token = mintBox('alice', Date.now() + 60_000, 'other-secret');
|
||||||
|
expect(verifyBoxTerminalToken(token, 'alice', secret)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects malformed input and an empty secret', () => {
|
||||||
|
expect(verifyBoxTerminalToken('garbage', 'alice', secret)).toBe(false);
|
||||||
|
expect(verifyBoxTerminalToken('', 'alice', secret)).toBe(false);
|
||||||
|
expect(
|
||||||
|
verifyBoxTerminalToken(mintBox('alice', Date.now() + 1000, ''), 'alice', ''),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a 3-part job token (cross-scheme confusion guard)', () => {
|
||||||
|
const payload = `${Date.now() + 60_000}.alice`;
|
||||||
|
const sig = createHmac('sha256', secret).update(payload).digest('hex');
|
||||||
|
const jobToken = `${payload}.${sig}`;
|
||||||
|
expect(verifyBoxTerminalToken(jobToken, 'alice', secret)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user