windmill-labs/windmill · error
signature mismatch
Error message
signature mismatch
What it means
`AuthedClient::verify` computes an HMAC-SHA256 over `v0:<timestamp>:<body>` with the shared secret and compares it hex-encoded (`v0=<hex>`) against the provided signature. A mismatch means either the body bytes differ from what was signed, the timestamp differs, or the wrong secret/algorithm was used. Thrown from `validate_view_token` when verifying a signed view-token payload.
Source
Thrown at backend/windmill-api/src/oauth2_oss.rs:193
#[cfg(not(feature = "private"))]
pub struct SlackVerifier {
mac: HmacSha256,
}
#[cfg(not(feature = "private"))]
impl SlackVerifier {
pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {
HmacSha256::new_from_slice(secret.as_ref())
.map(|mac| SlackVerifier { mac })
.map_err(|_| anyhow::anyhow!("invalid secret"))
}
pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {
let basestring = format!("v0:{}:{}", ts, body);
let mut mac = self.mac.clone();
mac.update(basestring.as_bytes());
let sig = format!("v0={}", hex::encode(mac.finalize().into_bytes()));
if sig != exp_sig {
Err(anyhow::anyhow!("signature mismatch"))?;
}
Ok(())
}
}
View on GitHub (pinned to e474e8803c)
Solutions
- Recompute the signature over the EXACT byte string that will be verified: sign "v0:{ts}:{body}" with the same secret and hex-encode as v0=<hex>.
- Confirm both sides use the same secret and HMAC-SHA256.
- Ensure the ts string is byte-identical (no reformatting of timestamps).
- Regenerate the view token rather than hand-editing its body; re-check any intermediary that could rewrite the body.
Example fix
// before
const sig = 'v0=' + hmacSha256(otherSecret, ts + body); // wrong format & secret
// after
const base = `v0:${ts}:${JSON.stringify(body)}`; // exact serialized body sent
const sig = 'v0=' + crypto.createHmac('sha256', sharedSecret).update(base).digest('hex'); Defensive patterns
Strategy: try-catch
Validate before calling
const base = `v0:${ts}:${bodyString}`;
const expected = 'v0=' + crypto.createHmac('sha256', secret).update(base).digest('hex');
if (expected !== expSig) throw new Error('local signature check failed before sending'); Type guard
function looksLikeSig(s) { return typeof s === 'string' && /^v0=[0-9a-f]{64}$/.test(s); } Try / catch
try {
await validateViewToken(token);
} catch (e) {
if (String(e).includes('signature mismatch')) {
// regenerate token with the current secret and exact body bytes
} else throw e;
} Prevention
- Sign the exact serialized body you transmit; never re-serialize after signing
- Keep the HMAC secret in sync across signer and verifier
- Use hex-encoded v0=<hex> HMAC-SHA256 output
- Never hand-edit signed payloads
When it happens
Trigger: Calling validate_view_token (which invokes verify at backend/windmill-api/src/oauth2_oss.rs:193) with an exp_sig that does not equal HMAC(secret, "v0:{ts}:{body}") — altered body, different ts, wrong secret, or re-signed payload with a different key.
Common situations: Payload mutated after signing (proxy adding/removing fields, whitespace, key reordering); clock/timestamp string mismatch between signer and verifier; rotating the secret on one side only; base64-vs-hex confusion on the client; encoding differences (JSON serialization order).
Related errors
- Invalid signature
- signature mismatch
- bundle produced no javascript:\n${buildOutput}
- raw app path ${JSON.stringify(relPath)} escapes the app fold
- Offline replay: this page renders a recording and cannot cal
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/9e364bd6a3641e7b.
Report an issue: GitHub.