Build API v2
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import {
|
||||
computeSignature,
|
||||
buildCanonicalRequest,
|
||||
verifyRequestSignature,
|
||||
_clearNonces
|
||||
} from './v2-signature';
|
||||
|
||||
const SECRET = 'test-onboarding-secret';
|
||||
|
||||
function signedRequest(opts: {
|
||||
now?: Date;
|
||||
timestampOffsetSec?: number;
|
||||
tamper?: Partial<Record<'signature' | 'nonce' | 'timestamp' | 'body', string>>;
|
||||
}) {
|
||||
const method = 'POST';
|
||||
const path = '/api/v2/automations/create/account';
|
||||
const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000));
|
||||
const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00';
|
||||
const body = '{"email":"user@example.com","subscription_level":"Basic"}';
|
||||
|
||||
let ts = timestamp;
|
||||
let nonceValue = nonce;
|
||||
|
||||
if (opts.tamper?.timestamp) ts = opts.tamper.timestamp;
|
||||
if (opts.tamper?.nonce) nonceValue = opts.tamper.nonce;
|
||||
|
||||
let sig = computeSignature(SECRET, buildCanonicalRequest(method, path, ts, nonceValue, body));
|
||||
|
||||
if (opts.tamper?.body)
|
||||
sig = computeSignature(
|
||||
SECRET,
|
||||
buildCanonicalRequest(method, path, ts, nonceValue, opts.tamper.body)
|
||||
);
|
||||
if (opts.tamper?.signature) sig = opts.tamper.signature;
|
||||
|
||||
const headers = new Headers({
|
||||
'content-type': 'application/json',
|
||||
'x-timestamp': ts,
|
||||
'x-nonce': nonceValue,
|
||||
'x-signature': sig
|
||||
});
|
||||
|
||||
return { method, path, headers, rawBody: body };
|
||||
}
|
||||
|
||||
describe('verifyRequestSignature', () => {
|
||||
beforeEach(() => {
|
||||
_clearNonces();
|
||||
});
|
||||
|
||||
it('accepts a correctly signed request', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a request with a wrong signature', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({
|
||||
tamper: { signature: 'deadbeef'.repeat(8) }
|
||||
});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.reason).toBe('invalid signature');
|
||||
});
|
||||
|
||||
it('rejects a request signed with the wrong secret', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({});
|
||||
const result = verifyRequestSignature({
|
||||
method,
|
||||
path,
|
||||
headers,
|
||||
rawBody,
|
||||
secret: 'other-secret'
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing signature headers', () => {
|
||||
const { method, path, rawBody } = signedRequest({});
|
||||
const result = verifyRequestSignature({
|
||||
method,
|
||||
path,
|
||||
headers: new Headers(),
|
||||
rawBody,
|
||||
secret: SECRET
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an expired timestamp (>300s skew)', () => {
|
||||
const now = new Date();
|
||||
const { method, path, headers, rawBody } = signedRequest({
|
||||
tamper: { timestamp: String(Math.floor(now.getTime() / 1000) - 600) }
|
||||
});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET, now });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.reason).toBe('expired timestamp');
|
||||
});
|
||||
|
||||
it('accepts a timestamp within the 300s skew', () => {
|
||||
const now = new Date();
|
||||
const { method, path, headers, rawBody } = signedRequest({
|
||||
tamper: { timestamp: String(Math.floor(now.getTime() / 1000) - 120) }
|
||||
});
|
||||
const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET, now });
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a replayed nonce', () => {
|
||||
const { method, path, headers, rawBody } = signedRequest({});
|
||||
expect(verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET }).ok).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
const replay = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET });
|
||||
expect(replay.ok).toBe(false);
|
||||
if (!replay.ok) expect(replay.reason).toBe('replayed nonce');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user