AutomationFlowsGeneral › Automation Note Signed Webhook Hmac Guard

Automation Note Signed Webhook Hmac Guard

AUTOMATION NOTE Signed Webhook HMAC Guard. Uses crypto. Webhook trigger; 7 nodes.

Webhook trigger★★★★☆ complexity7 nodesCrypto
General Trigger: Webhook Nodes: 7 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "AutomationNoteWebhookHmacGuard01",
  "name": "AUTOMATION NOTE Signed Webhook HMAC Guard",
  "description": null,
  "active": false,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "content": "## Verify a signed webhook before business logic\n\n### Who this is for\nAutomation teams receiving server-to-server webhooks from a provider that signs the exact request body with HMAC-SHA256.\n\n### Contract used by this template\n- Header: `X-Automation-Signature: sha256=<hex digest; comparison is case-insensitive>`\n- Signature input: the exact raw request body bytes\n- Body fields: `event_id`, signed Unix timestamp `sent_at`, and `payload`\n- Freshness: no more than 300 seconds old or 60 seconds in the future\n\n### Set up\n1. Keep the workflow inactive after import.\n2. Create an n8n Crypto credential with the shared HMAC secret.\n3. Assign it to **Compute Body HMAC**.\n4. Match the provider's exact header, canonical string, digest encoding, and timestamp rules.\n5. Add business nodes only after the true output of **Is Request Authentic and Fresh**.\n\n### Boundaries\nThe timestamp limits replay time but does not stop duplicate delivery inside the five-minute window. Add persistent event-ID deduplication for strict once-only handling. Apply rate and body-size limits before n8n. Generic `401` responses avoid exposing which check failed. The public workflow contains no credential or real data.",
        "height": 720,
        "width": 620,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -700,
        -300
      ],
      "id": "42c702f5-c54d-4d06-a1a3-9f550bc9c117",
      "name": "Template Guide"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "signed-webhook-hmac-guard",
        "authentication": "none",
        "responseMode": "responseNode",
        "options": {
          "rawBody": true
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        120
      ],
      "id": "a676a3b6-9f00-4b8f-8331-f355e509939f",
      "name": "Receive Signed Webhook"
    },
    {
      "parameters": {
        "action": "hmac",
        "binaryData": true,
        "binaryPropertyName": "data",
        "type": "SHA256",
        "dataPropertyName": "expected_signature",
        "encoding": "hex"
      },
      "type": "n8n-nodes-base.crypto",
      "typeVersion": 2,
      "position": [
        260,
        120
      ],
      "id": "08dc63de-1991-4d3f-a5ad-057096fd093a",
      "name": "Compute Body HMAC"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst headers = input.headers ?? {};\nconst received = typeof headers['x-automation-signature'] === 'string'\n  ? headers['x-automation-signature'].trim().toLowerCase()\n  : '';\nconst digest = typeof input.expected_signature === 'string'\n  ? input.expected_signature.toLowerCase()\n  : '';\nconst expected = `sha256=${digest}`;\nconst signatureFormatValid = /^sha256=[a-f0-9]{64}$/.test(received) && /^sha256=[a-f0-9]{64}$/.test(expected);\nlet signatureMismatch = 1;\nif (signatureFormatValid) {\n  signatureMismatch = 0;\n  for (let index = 0; index < expected.length; index += 1) {\n    signatureMismatch |= expected.charCodeAt(index) ^ received.charCodeAt(index);\n  }\n}\nconst body = input.body ?? {};\nconst sentAt = body.sent_at;\nconst now = Math.floor(Date.now() / 1000);\nconst timestampValid = Number.isInteger(sentAt) && sentAt >= now - 300 && sentAt <= now + 60;\nconst eventId = typeof body.event_id === 'string' ? body.event_id : '';\nconst eventIdValid = /^evt_[A-Za-z0-9-]{8,64}$/.test(eventId);\nconst contentType = typeof headers['content-type'] === 'string' ? headers['content-type'].toLowerCase() : '';\nconst contentTypeValid = contentType.startsWith('application/json');\nconst signatureValid = signatureFormatValid && signatureMismatch === 0;\nreturn [{ json: {\n  accepted: signatureValid && timestampValid && eventIdValid && contentTypeValid,\n  event_id: eventIdValid ? eventId : null,\n  authenticated: signatureValid,\n  fresh: timestampValid,\n  age_seconds: Number.isInteger(sentAt) ? now - sentAt : null,\n  replay_window_seconds: 300\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        520,
        120
      ],
      "id": "2af5b1f2-4074-4c9b-9880-f81e64d9229c",
      "name": "Validate Signature and Timestamp"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.accepted }}",
              "operation": "equal",
              "value2": true
            }
          ]
        },
        "combineOperation": "all"
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        780,
        120
      ],
      "id": "e01cced2-e2e0-4bc9-9f24-ed02e33e3e70",
      "name": "Is Request Authentic and Fresh"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: true, status: 'accepted', authenticated: true, event_id: $json.event_id, replay_window_seconds: $json.replay_window_seconds } }}",
        "options": {
          "responseCode": 200,
          "responseHeaders": {
            "entries": [
              {
                "name": "Cache-Control",
                "value": "no-store"
              },
              {
                "name": "X-Content-Type-Options",
                "value": "nosniff"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        1040,
        40
      ],
      "id": "59e351df-2536-4421-998b-28d327ee0380",
      "name": "Accept Authenticated Request"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: false, status: 'rejected', reason: 'invalid_or_stale_signature' } }}",
        "options": {
          "responseCode": 401,
          "responseHeaders": {
            "entries": [
              {
                "name": "Cache-Control",
                "value": "no-store"
              },
              {
                "name": "X-Content-Type-Options",
                "value": "nosniff"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        1040,
        200
      ],
      "id": "6e298e1f-e13d-4c46-b147-44161e10f312",
      "name": "Reject Invalid or Stale Request"
    }
  ],
  "connections": {
    "Receive Signed Webhook": {
      "main": [
        [
          {
            "node": "Compute Body HMAC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Body HMAC": {
      "main": [
        [
          {
            "node": "Validate Signature and Timestamp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Signature and Timestamp": {
      "main": [
        [
          {
            "node": "Is Request Authentic and Fresh",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Request Authentic and Fresh": {
      "main": [
        [
          {
            "node": "Accept Authenticated Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject Invalid or Stale Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "nodeGroups": [],
  "versionId": "023c2355-740c-4b7c-823e-971e62ab3206",
  "activeVersionId": "023c2355-740c-4b7c-823e-971e62ab3206",
  "sourceWorkflowId": null,
  "tags": [],
  "versionMetadata": {
    "name": null,
    "description": null
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

AUTOMATION NOTE Signed Webhook HMAC Guard. Uses crypto. Webhook trigger; 7 nodes.

Source: https://github.com/rains-hori/n8n-automation-recipes/blob/main/workflows/webhook-hmac-guard/workflow.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Webhook Filemaker. Uses crypto, moveBinaryData, respondToWebhook, filemaker. Webhook trigger; 11 nodes.

Crypto, Move Binary Data, Filemaker
General

Validate Seatable Webhooks with HMAC SHA256 Authentication. Uses respondToWebhook, crypto, noOp, stickyNote. Webhook trigger; 7 nodes.

Crypto
General

Validate Seatable Webhooks with HMAC SHA256 Authentication. Uses crypto. Webhook trigger; 7 nodes.

Crypto
General

A clean, extensible REST-style API routing template for n8n webhooks with up to 3 path levels. Serves API routes via Webhooks with path variables Normalizes incoming requests into "global" REQUEST and

General

PUQ Docker NextCloud deploy. Uses respondToWebhook, stickyNote, httpRequest, ssh. Webhook trigger; 44 nodes.

HTTP Request, Ssh