mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
fix(mattermost): sort context keys in HMAC token generation
Mattermost reorders context keys when storing and returning interactive message payloads. Without stable key ordering, JSON.stringify produces different output for the same context, causing HMAC verification to fail on button clicks. Sort keys before serialization in generateInteractionToken so tokens remain valid regardless of key order. Add tests covering key reordering.
This commit is contained in:
committed by
Muhammed Mukhthar CM
parent
e3509678dc
commit
68fe16e053
@@ -81,6 +81,28 @@ describe("generateInteractionToken / verifyInteractionToken", () => {
|
||||
const t2 = generateInteractionToken(context);
|
||||
expect(t1).toBe(t2);
|
||||
});
|
||||
|
||||
it("produces the same token regardless of key order", () => {
|
||||
const contextA = { action_id: "do_now", tweet_id: "123", action: "do" };
|
||||
const contextB = { action: "do", action_id: "do_now", tweet_id: "123" };
|
||||
const contextC = { tweet_id: "123", action: "do", action_id: "do_now" };
|
||||
const tokenA = generateInteractionToken(contextA);
|
||||
const tokenB = generateInteractionToken(contextB);
|
||||
const tokenC = generateInteractionToken(contextC);
|
||||
expect(tokenA).toBe(tokenB);
|
||||
expect(tokenB).toBe(tokenC);
|
||||
});
|
||||
|
||||
it("verifies a token when Mattermost reorders context keys", () => {
|
||||
// Simulate: token generated with keys in one order, verified with keys in another
|
||||
// (Mattermost reorders context keys when storing/returning interactive message payloads)
|
||||
const originalContext = { action_id: "bm_do", tweet_id: "999", action: "do" };
|
||||
const token = generateInteractionToken(originalContext);
|
||||
|
||||
// Mattermost returns keys in alphabetical order (or any arbitrary order)
|
||||
const reorderedContext = { action: "do", action_id: "bm_do", tweet_id: "999" };
|
||||
expect(verifyInteractionToken(reorderedContext, token)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Callback URL registry ────────────────────────────────────────────
|
||||
@@ -244,6 +266,25 @@ describe("buildButtonAttachments", () => {
|
||||
const { _token, ...contextWithoutToken } = ctx;
|
||||
expect(verifyInteractionToken(contextWithoutToken, token)).toBe(true);
|
||||
});
|
||||
|
||||
it("generates tokens that verify even when Mattermost reorders context keys", () => {
|
||||
const result = buildButtonAttachments({
|
||||
callbackUrl: "http://localhost/cb",
|
||||
buttons: [{ id: "do_action", name: "Do", context: { tweet_id: "42", category: "ai" } }],
|
||||
});
|
||||
|
||||
const ctx = result[0].actions![0].integration.context;
|
||||
const token = ctx._token as string;
|
||||
|
||||
// Simulate Mattermost returning context with keys in a different order
|
||||
const reordered: Record<string, unknown> = {};
|
||||
const keys = Object.keys(ctx).filter((k) => k !== "_token");
|
||||
// Reverse the key order to simulate reordering
|
||||
for (const key of keys.reverse()) {
|
||||
reordered[key] = ctx[key];
|
||||
}
|
||||
expect(verifyInteractionToken(reordered, token)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── isLocalhostRequest ───────────────────────────────────────────────
|
||||
|
||||
@@ -82,7 +82,8 @@ export function getInteractionSecret(): string {
|
||||
|
||||
export function generateInteractionToken(context: Record<string, unknown>): string {
|
||||
const secret = getInteractionSecret();
|
||||
const payload = JSON.stringify(context);
|
||||
// Sort keys for stable serialization — Mattermost may reorder context keys
|
||||
const payload = JSON.stringify(context, Object.keys(context).sort());
|
||||
return createHmac("sha256", secret).update(payload).digest("hex");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user