Skip to main content

Authentication

The publisher and subscriber must agree on the same auth mode. SYNC_AUTH_MODE (hmac or token) is read by both bundles from src/shared/config.ts, and the modes do not cross-accept: a token-mode subscriber rejects HMAC-signed requests and vice-versa.

ModeHeadersNotes
hmac (default)x-sync-timestamp, x-sync-signaturePer-request HMAC-SHA256 of <timestamp>.<rawBody> keyed with the shared secret. Replay-protected by timestamp tolerance plus an in-memory exact-request replay cache. Every retry re-signs with a fresh timestamp.
tokenx-sync-tokenStatic shared-secret bearer token. Simpler; use only over TLS or another protected transport. Delivery remains idempotent via eventId / entityRevision, but token mode does not reject request replay at the auth boundary.

Requests must use Content-Type: application/json. Unsupported Content-Encoding values are rejected before application.

The signature is HMAC_SHA256(sharedSecret, "<timestamp>.<rawBody>"), hex-encoded.

  • <timestamp> is a Unix-millisecond integer sent in x-sync-timestamp.
  • <rawBody> is the exact bytes sent on the wire. The publisher signs the JSON string it serialized. The subscriber reads n8n's global rawBodyReader value (req.rawBody) when available, otherwise it reads the unread request stream.
  • Fail-closed parsing — in HMAC mode the subscriber does not verify a re-serialized req.body. If only a pre-parsed body remains, the request fails closed because the exact signed bytes are no longer available. If both req.rawBody and req.body exist, req.body is ignored and the raw bytes are parsed and applied.
  • Replay protection — the subscriber rejects a request when the timestamp is outside SYNC_SIGNATURE_TOLERANCE_MS (default 300000 ms = 5 minutes). It also rejects an exact re-send of the same signed request while an identical request is in flight and after a successful application while the process-local replay cache remembers it.
  • Re-signing on retry — every delivery attempt (including retries inside sendSyncEvent) generates a fresh <timestamp>.<rawBody> pair and re-signs it. A retried request never reuses a previous signature.
  • Retryable failures release reservations — parse, validation, and application failures release the replay reservation so the exact request can be retried.
# publisher
export SYNC_AUTH_MODE=hmac
export SYNC_SHARED_SECRET=<32+-char-random-string>

# subscriber — must be identical
export SYNC_AUTH_MODE=hmac
export SYNC_SHARED_SECRET=<32+-char-random-string>

Why raw bytes matter​

A JSON body that round-trips through JSON.parse(req.body) and JSON.stringify(...) is not byte-identical to the original in general (key reordering, whitespace). HMAC over a re-serialized body could verify bytes different from what is applied. The subscriber therefore authenticates the exact raw bytes first, then parses and applies those same bytes. See src/shared/body.ts and src/shared/auth.ts for the implementation.

Token mode​

Static shared-secret bearer. There is no signing, no replay protection, no timestamp. Use it only when the publisher → subscriber hop is over mTLS or another protected transport.

# publisher
export SYNC_AUTH_MODE=token
export SYNC_SHARED_SECRET=<any-shared-string>

# subscriber — must be identical
export SYNC_AUTH_MODE=token
export SYNC_SHARED_SECRET=<any-shared-string>

The subscriber compares x-sync-token against the shared secret using a constant-time equality to avoid timing side-channels.

Gotchas​

  • Modes do not cross-accept. A token-mode subscriber rejects hmac-signed requests, and an hmac-mode subscriber rejects token-bearing requests. Both sides must agree on SYNC_AUTH_MODE.
  • SYNC_SHARED_SECRET is required in both modes — it is the HMAC key in hmac mode and the bearer token in token mode.
  • Token mode may reuse parsed JSON. Authentication does not depend on raw bytes in token mode, but the subscriber still enforces JSON content type and request size limits.
  • Replay rejection is process-local. HMAC replay cache entries are not shared across OS processes and do not survive restart. Durable ordering still makes stale re-delivery a no-op after cache expiry or restart.

Reference​