vhook-js
Version:
Verifyable Webhooks
147 lines (122 loc) • 5.73 kB
JavaScript
// test/vhook.test.js
const assert = require('assert');
const vhook = require('../vhook.js');
const jwt = require('jsonwebtoken');
describe('VHooks Module', function () {
let keys;
let privateKey;
let publicKey;
let token;
let payload;
let now
before(async function () {
// Generate key pair before running tests
keys = await vhook.create_vhook_keypair();
privateKey = keys.privateKey.pem;
publicKey = keys.publicKey.pem;
now = Math.floor(Date.now()/1000);
});
describe('create_vhook_keypair()', function () {
it('should generate a valid RSA key pair', function () {
assert.ok(keys.privateKey);
assert.ok(keys.publicKey);
assert.ok(keys.privateKey.pem);
assert.ok(keys.publicKey.pem);
// Check that keys are in PEM format
assert.ok(keys.privateKey.pem.includes('-----BEGIN RSA PRIVATE KEY-----'));
assert.ok(keys.publicKey.pem.includes('-----BEGIN PUBLIC KEY-----'));
});
});
describe('create_vhook()', function () {
it('should create a signed VHook token', function () {
payload = {
issuer: 'https://yourservice.com',
audience: 'https://receiver-service.com/webhook-endpoint',
origin: 'customer',
event: 'customer.updated',
data: {
customer_id: '12345',
name: 'Jane Doe',
email: 'jane.doe@example.com',
},
issued_at: now,
expiration: now + 60, // Expires in 60 seconds
message_id: 'test-message-id',
};
token = vhook.create_vhook(payload, privateKey);
assert.strictEqual(typeof token, 'string');
// Token should have three parts separated by dots
assert.strictEqual(token.split('.').length, 3);
});
it('should include msg_id in the payload', function () {
const decoded = jwt.decode(token);
assert.ok(decoded.jti);
assert.strictEqual(typeof decoded.jti, 'string');
});
});
describe('decode_vhook()', function () {
it('should decode and verify the VHook token', function () {
const decodedPayload = vhook.decode_vhook(token, publicKey);
// Check that the decoded payload matches the original payload
assert.deepStrictEqual(decodedPayload.origin, payload.origin);
assert.deepStrictEqual(decodedPayload.event, payload.event);
assert.deepStrictEqual(decodedPayload.data, payload.data);
// Check that standard claims are present
assert.strictEqual(decodedPayload.issuer, payload.issuer);
assert.strictEqual(decodedPayload.audience, payload.audience);
// Compare date fields as Date objects
assert.deepStrictEqual(decodedPayload.expiration, new Date(payload.expiration * 1000));
assert.deepStrictEqual(decodedPayload.issued_at, new Date(payload.issued_at * 1000));
// Ensure message_id is present and matches
assert.strictEqual(decodedPayload.message_id, payload.message_id);
});
it('should throw an error for invalid signature', function () {
// Tamper with the token
const parts = token.split('.');
const tamperedPayload = Buffer.from(parts[1], 'base64').toString('utf8');
const tamperedPayloadObj = JSON.parse(tamperedPayload);
tamperedPayloadObj.origin = 'attacker';
const newPayload = Buffer.from(JSON.stringify(tamperedPayloadObj)).toString('base64').replace(/=/g, '');
const tamperedToken = `${parts[0]}.${newPayload}.${parts[2]}`;
assert.throws(() => {
vhook.decode_vhook(tamperedToken, publicKey);
}, /invalid signature/);
});
it('raw_token should contain the same info as vhook', function () {
const decodedPayload = vhook.decode_vhook(token, publicKey);
const raw_token = decodedPayload.raw_token;
// Check that the decoded payload matches the original payload
assert.deepStrictEqual(decodedPayload.origin, raw_token.origin);
assert.deepStrictEqual(decodedPayload.event, raw_token.event);
assert.deepStrictEqual(decodedPayload.data, raw_token.data);
// Check that standard claims are present
assert.strictEqual(decodedPayload.issuer, raw_token.iss);
assert.strictEqual(decodedPayload.audience, raw_token.aud);
// Compare date fields as Date objects
assert.deepStrictEqual(decodedPayload.expiration, new Date(raw_token.exp * 1000));
assert.deepStrictEqual(decodedPayload.issued_at, new Date(raw_token.iat * 1000));
// Ensure message_id is present and matches
assert.strictEqual(decodedPayload.message_id, raw_token.jti);
});
});
describe('decode_vhook_without_validation()', function () {
it('should decode the VHook token without verifying the signature', function () {
const decodedPayload = vhook.decode_vhook_without_validation(token);
assert.deepStrictEqual(decodedPayload.origin, payload.origin);
assert.deepStrictEqual(decodedPayload.event, payload.event);
assert.deepStrictEqual(decodedPayload.data, payload.data);
assert.ok(decodedPayload.message_id);
});
it('should decode a tampered token without error', function () {
// Use the previously tampered token
const parts = token.split('.');
const tamperedPayload = Buffer.from(parts[1], 'base64').toString('utf8');
const tamperedPayloadObj = JSON.parse(tamperedPayload);
tamperedPayloadObj.origin = 'attacker';
const newPayload = Buffer.from(JSON.stringify(tamperedPayloadObj)).toString('base64').replace(/=/g, '');
const tamperedToken = `${parts[0]}.${newPayload}.${parts[2]}`;
const decodedPayload = vhook.decode_vhook_without_validation(tamperedToken);
assert.strictEqual(decodedPayload.origin, 'attacker');
});
});
});