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.clientSecretView on GitHub (pinned to d3aa8bea5b)
Solutions
- Configure the correct LinkedIn webhook secret (webhookSecret or clientSecret) for the app
- Ensure rawBody capture middleware runs before JSON parsing on this route
- Confirm no proxy rewrites the body or drops the signature header
- 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
- Configure webhookSecret (or clientSecret) before enabling the endpoint
- Preserve raw bytes of the body for HMAC
- Monitor 401 rates to detect secret rotation mismatches early
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.