hcengineering/platform · error

Invalid hook token

Error message

Invalid hook token

What it means

The MTA webhook handler in pod-mail-worker validates an incoming webhook by comparing the 'x-hook-token' request header against the configured hookToken (config.hookToken). When the token is configured and the header is missing, malformed, or does not exactly match, the handler rejects the request with 'Invalid hook token' to prevent unauthorized email hook injection. It is a security gate, not an internal fault.

Source

Thrown at services/mail/pod-mail-worker/src/handlerMta.ts:41

  isHulyMessage,
  generateNewEmailId,
  MailHeader
} from '@hcengineering/mail-common'
import { getClient as getAccountClient } from '@hcengineering/account-client'
import { createRestTxOperations } from '@hcengineering/api-client'

import { mailServiceToken, baseConfig, kvsClient } from './client'
import config from './config'
import { MtaMessage, HulyMessageType } from './types'
import { getHeader, parseContent } from './utils'
import { decodeEncodedWords } from './decode'

export async function handleMtaHook (req: Request, res: Response, ctx: MeasureContext): Promise<void> {
  try {
    if (config.hookToken !== undefined) {
      const token = req.headers['x-hook-token']
      if (token !== config.hookToken) {
        throw new Error('Invalid hook token')
      }
    }

    const mta: MtaMessage = req.body

    const headers: string[] = mta.message.headers.map((header) => header[0].trim()) ?? []
    if (isHulyMessage(headers)) {
      return
    }

    const from: EmailContact = getEmailContact(mta.envelope.from.address)
    if (config.ignoredAddresses.includes(from.email)) {
      return
    }
    const fromHeader = getHeader(mta, MailHeader.From)
    if (fromHeader !== undefined) {
      const { firstName, lastName } = extractContactName(ctx, fromHeader, from.email)
      from.firstName = firstName

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Set the 'x-hook-token' header on the MTA webhook request to exactly match config.hookToken (HOOK_TOKEN env var)
  2. If tokens were rotated, redeploy/reconfigure the MTA side with the new token
  3. If you intend no auth (dev only), unset config.hookToken so the check is skipped
  4. Log/inspect the received header value (carefully, no secrets in logs) to confirm exact match, including whitespace

Example fix

// before
curl -X POST https://mta-hook/url -d '{...}'
// after
curl -X POST https://mta-hook/url -H "x-hook-token: $HOOK_TOKEN" -d '{...}'
Defensive patterns

Strategy: validation

Validate before calling

function validateHookRequest(req: Request, expectedToken?: string): void {
  if (expectedToken !== undefined && req.headers['x-hook-token'] !== expectedToken) {
    throw new Error('Invalid hook token')
  }
}

Type guard

function hasValidHookToken(req: Request, config: { hookToken?: string }): boolean {
  return config.hookToken === undefined || req.headers['x-hook-token'] === config.hookToken
}

Try / catch

try {
  await handleMtaHook(req, res, ctx)
} catch (e) {
  if (e.message === 'Invalid hook token') {
    res.status(401).send('unauthorized')
  } else throw e
}

Prevention

When it happens

Trigger: A POST to the MTA hook endpoint with: (1) no 'x-hook-token' header, (2) a token that differs from config.hookToken (typo, stale secret after rotation), or (3) the worker redeployed with a new HOOK_TOKEN env value while the MTA still sends the old one.

Common situations: Rotating the hook token in one deployment (worker or MTA) but not the other; token passed via query param instead of header; multiple MTA instances where only some were updated; local dev MTA not configured to send the header at all while the worker sets config.hookToken.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/bb29afd79ab4c350. Report an issue: GitHub.