Skip to Content
HMAC Authentication

HMAC Authentication

HMAC (Hash-based Message Authentication Code) provides request-level authentication for M3 Forge API endpoints. Every signed request includes a cryptographic signature that proves the sender possesses a shared secret — without transmitting the secret itself.

M3 Forge uses HMAC-SHA256 signatures for both incoming API requests (clients calling M3 Forge) and outgoing webhook deliveries (M3 Forge calling your endpoints).


How It Works

Each HMAC-signed request includes four headers:

HeaderDescriptionExample
X-Marie-TimestampUnix epoch seconds when the request was signed1711036800
X-Marie-NonceUUID v4 for replay prevention550e8400-e29b-41d4-a716-446655440000
X-Marie-SignatureHMAC-SHA256 hex digest prefixed with sha256=sha256=a1b2c3d4...
X-Marie-Key-IdPublic identifier for the signing keymsk_aBcDeFgHiJkLmNoP

Signature Construction

The signature is computed over a canonical message assembled from five components joined by newline characters:

{timestamp}\n{nonce}\n{METHOD}\n{path_and_query}\n{body}
ComponentRule
timestampSame value as X-Marie-Timestamp header
nonceSame value as X-Marie-Nonce header
METHODHTTP method, uppercased (GET, POST, PUT, DELETE)
path_and_queryRequest path including query string, no scheme or host (e.g. /api/trpc/workflows.list?batch=1)
bodyRaw request body bytes. Empty string "" for requests with no body.

Only application/json request bodies are supported for HMAC authentication. Requests using multipart/form-data or application/x-www-form-urlencoded are not covered.

Replay Protection

M3 Forge applies two layers of replay protection:

  1. Timestamp window — Requests are rejected if the signature timestamp differs from server time by more than 60 seconds.
  2. Nonce deduplication — The server tracks recently seen (keyId, nonce) pairs for 120 seconds. Duplicate nonces within this window are rejected. Since the nonce is part of the signed message, it cannot be stripped or altered without invalidating the signature.

Setup

Generate a signing key

Navigate to Settings → Signing Keys and click Generate Signing Key. Provide a name and select the scopes this key should have access to.

The secret is displayed only once after generation. Copy and store it securely — it cannot be retrieved later.

Store your credentials

Save both values in your application’s secret management system:

  • Key ID (msk_...) — Public identifier sent with every request. Safe to log.
  • Secret — 32-byte hex string used to compute signatures. Treat as a password.

Sign your requests

Include the four HMAC headers on every API request. See Signing Requests below for code examples in Python and TypeScript.


When HMAC is Applied

Incoming requests (clients → M3 Forge)

When a client sends a request to M3 Forge with the four HMAC headers present, the server verifies the signature before processing the request. HMAC authentication covers tRPC API routes (/api/trpc/*) where all business endpoints live.

REST routes (/api/auth/*, /api/oauth/*), SSE streams, and file uploads have their own authentication mechanisms and are not covered by HMAC.

Authentication precedence: Bearer token (JWT / mat_ API key) → Session cookie → HMAC signature. If a valid bearer token or session is already present, HMAC headers are ignored.

Outgoing requests (M3 Forge → your endpoints)

When you configure a trigger webhook with HMAC auth type, M3 Forge signs every outgoing delivery with the selected signing key. Your endpoint receives the four X-Marie-* headers and can verify the request originated from M3 Forge. See Verifying Signatures for receiver-side code.


Signing Requests

Use these examples to sign outgoing requests to M3 Forge from your application.

import hashlib import hmac import time import uuid import requests MARIE_KEY_ID = "msk_aBcDeFgHiJkLmNoP" MARIE_SECRET = "your-secret-hex-string" def sign_request(method: str, url: str, body: str = "") -> dict: """Generate HMAC signature headers for an M3 Forge API request.""" timestamp = str(int(time.time())) nonce = str(uuid.uuid4()) # Build the canonical message message = f"{timestamp}\n{nonce}\n{method.upper()}\n{url}\n{body}" # Compute HMAC-SHA256 signature = hmac.new( MARIE_SECRET.encode("utf-8"), message.encode("utf-8"), hashlib.sha256, ).hexdigest() return { "X-Marie-Timestamp": timestamp, "X-Marie-Nonce": nonce, "X-Marie-Signature": f"sha256={signature}", "X-Marie-Key-Id": MARIE_KEY_ID, } # Example: List workflows path = "/api/trpc/workflows.list?batch=1" headers = sign_request("GET", path) headers["Content-Type"] = "application/json" response = requests.get( f"https://your-instance.example.com{path}", headers=headers, ) print(response.json())

Verifying Signatures

When M3 Forge sends HMAC-signed webhook deliveries to your endpoint, verify the signature before processing the payload.

Extract the headers

Read the four X-Marie-* headers from the incoming request.

Reconstruct the canonical message

Build the same "{timestamp}\n{nonce}\n{METHOD}\n{path_and_query}\n{body}" string using the request data and the timestamp/nonce from the headers.

Compare signatures

Compute HMAC-SHA256 over the message using your copy of the shared secret and compare with the received signature. Always use a timing-safe comparison to prevent timing attacks.

import hashlib import hmac import time SHARED_SECRET = "your-secret-hex-string" MAX_AGE_SECONDS = 60 def verify_webhook(headers: dict, method: str, url: str, body: str) -> bool: """Verify an HMAC-signed request from M3 Forge.""" timestamp = headers.get("X-Marie-Timestamp", "") nonce = headers.get("X-Marie-Nonce", "") received_sig = headers.get("X-Marie-Signature", "") # Check timestamp freshness try: ts = int(timestamp) except ValueError: return False if abs(time.time() - ts) > MAX_AGE_SECONDS: return False # Strip the "sha256=" prefix if not received_sig.startswith("sha256="): return False received_hex = received_sig[7:] # Reconstruct and sign message = f"{timestamp}\n{nonce}\n{method.upper()}\n{url}\n{body}" expected_hex = hmac.new( SHARED_SECRET.encode("utf-8"), message.encode("utf-8"), hashlib.sha256, ).hexdigest() # Timing-safe comparison return hmac.compare_digest(expected_hex, received_hex)

Scope Model

HMAC signing keys carry an explicit list of allowed scopes. When a request authenticates via HMAC, the caller receives only the scopes assigned to the key — not the key owner’s full role permissions.

This means a leaked key is limited to its configured scopes. For example, a key with ["workflows:read", "runs:create"] can list workflows and trigger runs, but cannot modify workflows, access settings, or perform admin operations.

ConceptBehavior
Scopes are additiveA key can have one or many scopes
No privilege escalationHMAC keys never inherit admin access, even if the owner is an admin
Restricted scopesuser:impersonate, user:changeRole, settings:update, role:manage, and externalApp:manage cannot be assigned to HMAC keys

Choose the minimum set of scopes your integration needs. You can always add more scopes later without rotating the secret.


Key Management

Rotating secrets

Navigate to Settings → Signing Keys, select a key, and click Rotate Secret. This generates a new secret while keeping the same Key ID (msk_...). The old secret is immediately invalidated.

Rotate secrets during a maintenance window. Any in-flight requests signed with the old secret will be rejected after rotation.

Disabling keys

Toggle a key’s Enabled state to temporarily suspend authentication without deleting the key. Disabled keys reject all incoming requests and are not used for outgoing webhook signing.

Deleting keys

Deleting a key permanently removes it. Any webhooks configured with this key will fall back to unsigned delivery. This action cannot be undone.


Security Best Practices

  • Store secrets securely. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, environment variables) — never hard-code secrets in source code.
  • Rotate regularly. Rotate signing keys on a schedule (e.g., every 90 days) and immediately if a secret may have been exposed.
  • Use minimal scopes. Assign only the scopes your integration needs. Avoid broad scope sets.
  • Monitor usage. Check the Last Used timestamp in Settings to identify inactive keys that should be disabled or deleted.
  • Enforce HTTPS. HMAC protects against tampering, not eavesdropping. Always use HTTPS to prevent the signature headers and request body from being intercepted.
  • Validate on your side. When receiving webhooks from M3 Forge, always verify the signature before processing. Reject requests with expired timestamps or invalid signatures.

Troubleshooting

401 Unauthorized — Signature verification failed

CauseFix
Wrong secretVerify you’re using the correct secret for the Key ID in the request
Body mismatchSign the raw request body exactly as transmitted. JSON serialization differences (key ordering, whitespace) will produce different signatures
URL mismatchThe URL component must be the path and query string only — no scheme (https://) or host. Example: /api/trpc/workflows.list?batch=1
Method caseThe HTTP method must be uppercased in the canonical message: GET, POST, not get, post
Timestamp expiredSignatures expire after 60 seconds. Check that the signing machine’s clock is synchronized (NTP)
Nonce reusedEach request must use a unique nonce (UUID v4). Do not reuse nonces from previous requests

403 Forbidden — Insufficient scopes

The signing key does not have the required scope for the requested endpoint. Check the key’s allowed scopes in Settings → Signing Keys and add the missing scope.

401 Unauthorized — Key disabled or not found

Verify the key is enabled and the Key ID (X-Marie-Key-Id) matches an active key in your M3 Forge instance.

Clock drift issues

If your server’s clock is more than 60 seconds off from M3 Forge’s server time, all requests will be rejected. Use NTP to synchronize clocks:

# Check clock offset ntpdate -q pool.ntp.org # Sync (requires root) sudo ntpdate pool.ntp.org
Last updated on