yikart/AiToEarn · error

ChannelWebhookInvalidSignature

Error message

ChannelWebhookInvalidSignature

What it means

LinkedIn webhook provider rejects POSTs whose X-Hub-Signature-256 header does not match the HMAC-SHA256 of the raw body computed with the webhook secret. It logs a warning for AccountType.LinkedIn and responds 401 with a localized ChannelWebhookInvalidSignature message.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin-webhook.provider.ts:27

interface LinkedInWebhookChallengeQuery {
  challengeCode?: string
}

@Injectable()
export class LinkedInWebhookProvider implements PlatformWebhookHandler {
  private readonly logger = new Logger(LinkedInWebhookProvider.name)

  constructor(private readonly config: LinkedinConfig) {}

  async handle(request: Request, response: Response): Promise<void> {
    if (request.method === 'GET') {
      this.handleChallenge(request, response)
      return
    }
    if (!this.verify(request)) {
      this.logger.warn({ platform: AccountType.LinkedIn }, 'LinkedIn webhook signature invalid')
      response.status(401).send(getCodeMessage(ResponseCode.ChannelWebhookInvalidSignature, undefined, getLocale()))
      return
    }
    const payload: LinkedInWebhookPayload = LinkedInWebhookPayloadSchema.parse(request.body)
    this.logger.log({
      platform: AccountType.LinkedIn,
      eventCount: payload.events?.length ?? 0,
    }, 'LinkedIn webhook events received')
    response.status(200).json({ status: 'ok' })
  }

  private handleChallenge(request: Request, response: Response): void {
    const query = request.query as LinkedInWebhookChallengeQuery
    if (!query.challengeCode) {
      response.status(400).send(getCodeMessage(ResponseCode.ChannelWebhookChallengeCodeMissing, undefined, getLocale()))
      return
    }

    const secret = this.config.webhookSecret || this.config.clientSecret

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Configure the correct LinkedIn webhook secret (webhookSecret or clientSecret) for the app
  2. Ensure rawBody capture middleware runs before JSON parsing on this route
  3. Confirm no proxy rewrites the body or drops the signature header
  4. Re-register/refresh the webhook subscription in LinkedIn's developer portal

Example fix

// before
if (!this.config.clientSecret) throw new Error('missing') // verify silently fails downstream
// after
if (!this.config.webhookSecret && !this.config.clientSecret) {
  this.logger.error('LinkedIn webhook secret not configured')
}
Defensive patterns

Strategy: validation

Validate before calling

const sig = req.headers['x-hub-signature-256']
if (!sig?.startsWith('sha256=') || !rawBody || !process.env.LINKEDIN_WEBHOOK_SECRET) throw new Error('invalid LinkedIn signature')
const expected = createHmac('sha256', process.env.LINKEDIN_WEBHOOK_SECRET).update(rawBody).digest('hex')
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(sig.slice(7)))) throw new Error('signature mismatch')

Type guard

function hasLinkedInSignature(sig: unknown): sig is string {
  return typeof sig === 'string' && sig.startsWith('sha256=')
}

Prevention

When it happens

Trigger: POST to the LinkedIn webhook endpoint with missing/malformed/mismatched signature header, missing rawBody, or unset clientSecret/webhookSecret so verification cannot pass.

Common situations: LinkedIn application webhook secret not configured or rotated; body transformed by middleware before HMAC check; unsigned probes hitting the public URL; event payload forwarded through an intermediary that re-serializes JSON.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/41d7afe7d5866903. Report an issue: GitHub.