70 lines
2.7 KiB
JavaScript
70 lines
2.7 KiB
JavaScript
/**
|
|
* revokeToken() is reachable from the unauthenticated logout endpoints
|
|
* (POST /api/auth/logout, /gallery/logout, /customer-auth/logout). It used
|
|
* to base64-decode the payload without checking the signature and insert a
|
|
* row keyed on `${id}-${iat}-${type}` -- the same key isTokenRevoked()
|
|
* matches for real sessions. Anyone could therefore forge a payload naming
|
|
* another user's id, type and login second and log them out remotely, and
|
|
* with a far-future `exp` the row was never swept.
|
|
*
|
|
* The contract pinned here: only a token whose signature verifies under
|
|
* JWT_SECRET is written to revoked_tokens. Expired-but-genuine tokens are
|
|
* still accepted (logout must stay idempotent).
|
|
*/
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
process.env.JWT_SECRET = 'revocation-forgery-test-secret';
|
|
|
|
const inserted = [];
|
|
jest.mock('../../src/database/db', () => {
|
|
const dbFn = () => ({
|
|
insert(row) {
|
|
inserted.push(row);
|
|
return { onConflict: () => ({ ignore: async () => undefined, merge: async () => undefined }) };
|
|
},
|
|
});
|
|
return { db: dbFn };
|
|
});
|
|
jest.mock('../../src/utils/logger', () => ({
|
|
info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(),
|
|
}));
|
|
|
|
const { revokeToken } = require('../../src/utils/tokenRevocation');
|
|
|
|
const iat = Math.floor(Date.now() / 1000) - 60;
|
|
|
|
describe('revokeToken signature check', () => {
|
|
beforeEach(() => { inserted.length = 0; });
|
|
|
|
it('refuses a forged three-part token and writes nothing', async () => {
|
|
const forgedPayload = Buffer.from(JSON.stringify({
|
|
id: 1, iat, type: 'admin', exp: 9e9,
|
|
})).toString('base64');
|
|
const forged = `eyJhbGciOiJIUzI1NiJ9.${forgedPayload}.notasignature`;
|
|
|
|
const result = await revokeToken(forged, 'user_logout');
|
|
|
|
expect(result).toBe(false);
|
|
expect(inserted).toHaveLength(0);
|
|
});
|
|
|
|
it('refuses a token signed with a different secret', async () => {
|
|
const other = jwt.sign({ id: 1, iat, type: 'admin' }, 'some-other-secret', { expiresIn: '1h' });
|
|
expect(await revokeToken(other, 'user_logout')).toBe(false);
|
|
expect(inserted).toHaveLength(0);
|
|
});
|
|
|
|
it('revokes a genuine token', async () => {
|
|
const genuine = jwt.sign({ id: 1, iat, type: 'admin' }, process.env.JWT_SECRET, { expiresIn: '1h' });
|
|
expect(await revokeToken(genuine, 'user_logout')).toBe(true);
|
|
expect(inserted).toHaveLength(1);
|
|
expect(inserted[0].token_id).toBe(`1-${iat}-admin`);
|
|
});
|
|
|
|
it('still revokes a genuine token that has already expired', async () => {
|
|
const expired = jwt.sign({ id: 1, iat, type: 'admin', exp: iat + 1 }, process.env.JWT_SECRET);
|
|
expect(await revokeToken(expired, 'user_logout')).toBe(true);
|
|
expect(inserted).toHaveLength(1);
|
|
});
|
|
});
|