windmill-labs/windmill · error
Invalid signature
Error message
Invalid signature
What it means
Resume secrets (approval/approver links) are HMAC-signed; this error is thrown when mac.verify_slice fails, i.e. the provided signature does not match a freshly computed HMAC of job_id, resume_id and optional approver with the server's secret. It means the resume link or token is forged, corrupted, or signed with a different secret.
Source
Thrown at backend/windmill-api/src/jobs.rs:5239
}
async fn verify_suspended_secret(
w_id: &String,
db: &DB,
job_id: Uuid,
resume_id: u32,
approver: &QueryApprover,
secret: String,
) -> Result<(), Error> {
let key = get_workspace_key(w_id, db).await?;
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(to_anyhow)?;
mac.update(job_id.as_bytes());
mac.update(resume_id.to_be_bytes().as_ref());
if let Some(approver) = approver.approver.clone() {
mac.update(approver.as_bytes());
}
mac.verify_slice(hex::decode(secret)?.as_ref())
.map_err(|_| anyhow::anyhow!("Invalid signature"))?;
Ok(())
}
/* If the flow is currently waiting to be resumed (`FlowStatusModule::WaitingForEvents`)
* the suspend column must be set to the number of resume messages waited on.
*
* The flow's queue row is locked in this transaction because to avoid race conditions around
* the suspend column.
* That is, a job needs one event but it hasn't arrived, a worker counts zero events before
* entering WaitingForEvents. Then this message arrives but the job isn't in WaitingForEvents
* yet so the suspend counter isn't updated. Then the job enters WaitingForEvents expecting
* one event to arrive based on the count that is no longer correct. */
async fn resume_immediately_if_relevant<'c>(
flow: FlowInfo,
job_id: Uuid,
tx: &mut Transaction<'c, Postgres>,
) -> error::Result<()> {
Ok(View on GitHub (pinned to e474e8803c)
Solutions
- Request a fresh approval/resume link (re-trigger the resume request)
- Verify the URL's signature parameter was not truncated or escaped by the email client
- Check that the instance's HMAC secret was not changed after the link was generated
- Never hand-craft the URL; always use the link emitted by the server
Example fix
// before
const sig = url.searchParams.get('s').slice(0, 32); // corrupted
// after
const sig = url.searchParams.get('s'); // pass full hex signature verbatim Defensive patterns
Strategy: try-catch
Validate before calling
// ensure the URL carries a complete hex signature before calling
const sig = new URL(link).searchParams.get('signature');
if (!sig || !/^[0-9a-f]+$/i.test(sig)) throw new Error('malformed resume link'); Type guard
function hasValidSignature(params) {
return typeof params.signature === 'string' && /^[0-9a-f]{40,}$/i.test(params.signature);
} Try / catch
try {
await client.resume(flowId, resumeId, payload);
} catch (e) {
if (/Invalid signature/.test(e.message)) {
// link tampered/stale: request a fresh one, do not retry with the same link
await requestNewApprovalLink(flowId);
} else throw e;
} Prevention
- Always use the exact link emitted by the server
- Rotate HMAC secrets knowing all outstanding links invalidate
- Strip mail-client line wrapping before extracting the URL
When it happens
Trigger: Calling the anonymous approval/resume endpoint with a tampered signature parameter; using a resume link generated before the server secret was rotated; truncation/corruption of the hex-encoded signature in an email client.
Common situations: Email clients line-wrapping or HTML-escaping the signed link; rotating WINDMILL secrets in a running deployment so old links invalidate; manually constructing approval URLs.
Related errors
- signature mismatch
- 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/fa7a671645ea5807.
Report an issue: GitHub.