Skip to content

fix(monday): verify webhook signatures and fail closed on missing secret - #610

Merged
devjain32 merged 9 commits into
corsairdev:mainfrom
sushantlokhande14:fix/monday-webhook-verification
Aug 6, 2026
Merged

fix(monday): verify webhook signatures and fail closed on missing secret#610
devjain32 merged 9 commits into
corsairdev:mainfrom
sushantlokhande14:fix/monday-webhook-verification

Conversation

@sushantlokhande14

@sushantlokhande14 sushantlokhande14 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #581.

Two related problems, as described in the issue:

1. The handlers never verified anything. In item-created.ts, status-changed.ts and column-value-changed.ts the verification block was present but commented out, and execution fell straight through to the event logic — so any request matching the event shape was accepted, and itemCreated went on to write to the database.

// before — in all three handlers
// const verification = verifyMondayWebhookSignature(request, ctx.key);
// if (!verification.valid) { ... 401 ... }

const event = request.payload.event;   // <-- reached unconditionally

The block is now enabled in each handler, returning 401 before any DB write.

2. verifyMondayWebhookSignature failed open on an empty secret. if (!secret) return { valid: true } → now returns Missing webhook secret, matching the string and shape already used by the Spotify verifier after the same family was fixed there (#519, #520, #514).

challenge.ts is deliberately left alone

It has its own matcher (createMondayChallengeMatch) and is the subscription handshake Monday sends when the webhook is first registered — before any secret is exchanged — so verifying it would make webhook setup impossible. It performs no DB write and only echoes the challenge token back. Flagging it explicitly since "all handlers" could be read to include it.

Tests

Adds packages/monday/webhooks/webhooks.test.ts (9 tests, all passing):

Verifier — missing secret → Missing webhook secret; missing Authorization header; missing raw body; correctly-signed request → valid.

Handlers — for each of the three: a signature computed with a different key returns success: false / statusCode: 401. For itemCreated specifically:

  • forged signature → 401 and ctx.db.items.upsertByEntityId is never called
  • no secret configured → rejected and no upsert
  • correctly signed → success: true and the item is upserted with the expected payload

That covers all four boxes in the issue, including "no DB write" on the reject path.

One extra line: the jest config

packages/monday/jest.config.cjs maps corsair/http but not corsair/core. The handlers import logEventFromContext from corsair/core, so without that mapping the suite cannot import a handler at all — it dies with Cannot find module 'corsair/core' before running a single assertion. I added the one mapping, copying it verbatim from packages/cloudinary/jest.config.cjs, which is the plugin that already does handler-level webhook tests. Still inside packages/monday/**, so R1 scope holds.

Checklist

  • I have run pnpm lint and all checks pass — biome check clean on changed files; the repo's lint-staged hook ran clean on all three commits
  • I have run pnpm typecheck and there are no TypeScript errors — tsc --build packages/monday exits 0
  • I have run pnpm build and all packages build successfully
  • I have run pnpm test and all tests pass — green in CI; see note for local runs
  • I have added or updated tests where applicable
  • I have added or updated necessary documentation — none required, no public API or plugin option changed

On the test box. My suite passes 9/9. packages/monday/api.test.ts fails 11/11 on a clean checkout of main as well (it builds an auth header from an unset API key), so this branch changes nothing there:

clean main:   Test Suites: 1 failed, 1 total            Tests: 11 failed, 11 total
this branch:  Test Suites: 1 failed, 1 passed, 2 total  Tests: 11 failed, 9 passed, 20 total
                                     ^ +1 (mine)               ^ identical  ^ +9 (mine)

Screenshots / Demos (if applicable)

No UI. Local JWT verification only. Evidence is the unit tests + issue:
#581

Additional Notes

Summary by CodeRabbit

  • Security

    • Monday webhook authorization signatures are now verified using HS256 JWT validation.
    • Invalid, expired, malformed, or unauthorized requests receive a 401 response.
    • Bearer authorization is accepted regardless of capitalization.
  • Bug Fixes

    • Prevented unverified webhook events from being processed or persisted.
  • Tests

    • Added coverage for valid tokens, forged signatures, expired tokens, missing secrets, Bearer authorization, and rejected requests.

verifyMondayWebhookSignature returned { valid: true } for an empty
secret, matching the fail-open family already fixed in the Spotify, Zoom
and Slack verifiers (corsairdev#519, corsairdev#520, corsairdev#514).

Refs corsairdev#581
The verification block was commented out in all three event handlers, so
itemCreated, statusChanged and columnValueChanged accepted any request
that matched the event shape -- including forged ones -- and itemCreated
went on to write to the database.

Enable the existing check in each handler, returning 401 before any DB
write. The challenge handler is intentionally left unverified: it is the
subscription handshake Monday sends before a secret is exchanged, and it
performs no writes.

Refs corsairdev#581
Adds packages/monday/webhooks/webhooks.test.ts: the verifier's
fail-closed paths, and per-handler assertions that a forged signature
returns 401 and that itemCreated performs no upsert on rejection.

Also maps corsair/core in the package's jest config, as cloudinary
already does, so the handler modules resolve under ts-jest -- without it
the suite cannot import a handler at all.

Refs corsairdev#581
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
www Skipped Skipped Aug 6, 2026 2:09pm

Request Review

@github-actions github-actions Bot added the plugin Changes inside a plugin package label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52bf9863-4fd6-473c-bce3-6ba568051cec

📥 Commits

Reviewing files that changed from the base of the PR and between 872fca3 and 6ddbf31.

📒 Files selected for processing (3)
  • packages/monday/webhooks/tenant-matcher.ts
  • packages/monday/webhooks/types.ts
  • packages/monday/webhooks/webhooks.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/monday/webhooks/types.ts
  • packages/monday/webhooks/webhooks.test.ts

📝 Walkthrough

Walkthrough

Monday webhook handlers now verify Authorization JWTs before processing events. Invalid requests return HTTP 401. The verifier validates HS256 signatures, expiration, Bearer prefixes, and configured secrets. Tests cover verification and persistence behavior.

Changes

Monday webhook security

Layer / File(s) Summary
JWT verification contract
packages/monday/webhooks/types.ts, packages/monday/webhooks/tenant-matcher.ts
The verifier validates HS256 JWT structure, signatures, expiration, authorization prefixes, and configured secrets.
Handler signature enforcement
packages/monday/webhooks/item-created.ts, packages/monday/webhooks/status-changed.ts, packages/monday/webhooks/column-value-changed.ts
All three handlers verify signatures before processing events. Failed verification returns HTTP 401.
Verification tests and Jest wiring
packages/monday/webhooks/webhooks.test.ts, packages/monday/jest.config.cjs
Tests cover valid, missing, forged, expired, and Bearer-prefixed tokens. Handler tests verify rejected requests do not persist data. Jest resolves the local corsair/core implementation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Monday
  participant WebhookHandler
  participant verifyMondayWebhookSignature
  participant Persistence
  Monday->>WebhookHandler: Submit webhook request
  WebhookHandler->>verifyMondayWebhookSignature: Validate Authorization JWT
  verifyMondayWebhookSignature-->>WebhookHandler: Return validation result
  alt Signature is valid
    WebhookHandler->>Persistence: Persist event
    WebhookHandler-->>Monday: Return success
  else Signature is invalid
    WebhookHandler-->>Monday: Return HTTP 401
  end
Loading

Possibly related PRs

Suggested labels: bot:round-1

Suggested reviewers: devjain32, yuvrxj-afk

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the Monday webhook signature verification fix and the fail-closed behavior for missing secrets.
Linked Issues check ✅ Passed The PR satisfies issue #581 by verifying all three handlers, returning 401 before writes, rejecting empty secrets, and adding rejection tests.
Out of Scope Changes check ✅ Passed All changes support Monday webhook verification, testing, or required Jest configuration within packages/monday.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR replaces the incorrect body-HMAC comparison with HS256 JWT verification and enables fail-closed authentication in the three Monday event handlers.

  • Validates JWT structure, algorithm, signature, and expiration before event processing.
  • Rejects missing webhook secrets and authorization headers.
  • Adds handler-level tests covering forged, missing-secret, expired, Bearer-prefixed, and valid requests.
  • Adds the Jest module mapping needed to import corsair/core.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported raw-HMAC treatment of Monday JWTs has been replaced with signature verification over the JWT signing input.

Important Files Changed

Filename Overview
packages/monday/webhooks/types.ts Replaces raw-body HMAC comparison with HS256 JWT verification and fails closed when the webhook secret is absent.
packages/monday/webhooks/item-created.ts Gates item persistence on successful webhook JWT verification.
packages/monday/webhooks/status-changed.ts Rejects unauthenticated status-change events before event processing.
packages/monday/webhooks/column-value-changed.ts Rejects unauthenticated column-value events before event processing.
packages/monday/webhooks/webhooks.test.ts Adds verifier and handler tests covering accepted and rejected authentication paths.
packages/monday/jest.config.cjs Maps corsair/core so the new handler-level tests can load their runtime dependency.

Sequence Diagram

sequenceDiagram
  participant Monday
  participant Handler as Monday webhook handler
  participant Verifier as JWT verifier
  participant DB
  Monday->>Handler: Event with Authorization JWT
  Handler->>Verifier: Verify JWT using ctx.key
  alt Missing secret or invalid JWT
    Verifier-->>Handler: Invalid
    Handler-->>Monday: 401
  else Valid JWT
    Verifier-->>Handler: Valid
    Handler->>DB: Process or persist event
    Handler-->>Monday: Success
  end
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/monday-webh..." | Re-trigger Greptile

@ambikeesshh

Copy link
Copy Markdown
Collaborator

@greptileai review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/monday/webhooks/types.ts`:
- Around line 221-223: Update the token extraction logic around authHeader in
types.ts and the corresponding tenant-matching logic in tenant-matcher.ts to
recognize the Bearer scheme case-insensitively, while preserving the existing
raw-token behavior for headers without that scheme.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 23da8e1b-65eb-4733-a281-447b1f2be6fa

📥 Commits

Reviewing files that changed from the base of the PR and between 0d5b026 and 872fca3.

📒 Files selected for processing (2)
  • packages/monday/webhooks/types.ts
  • packages/monday/webhooks/webhooks.test.ts

Comment thread packages/monday/webhooks/types.ts Outdated
Comment on lines +221 to +223
const token = authHeader.startsWith('Bearer ')
? authHeader.slice('Bearer '.length)
: authHeader;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target context ---'
cat -n packages/monday/webhooks/types.ts | sed -n '190,245p'

printf '%s\n' '--- related symbols and tests ---'
rg -n --glob '!node_modules' 'authHeader|Bearer |bearer |JWT|jwt|webhook' packages/monday | head -250

Repository: corsairdev/corsair

Length of output: 10023


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- JWT verifier and surrounding types ---'
cat -n packages/monday/webhooks/types.ts | sed -n '115,230p'

printf '%s\n' '--- webhook authentication tests ---'
cat -n packages/monday/webhooks/webhooks.test.ts | sed -n '1,170p'

printf '%s\n' '--- tenant matcher implementation ---'
cat -n packages/monday/webhooks/tenant-matcher.ts | sed -n '1,90p'

printf '%s\n' '--- all callers of the two authentication helpers ---'
rg -n --glob '!node_modules' 'verifyMondayWebhookSignature|matchMondayTenantWebhook' packages/monday

Repository: corsairdev/corsair

Length of output: 13743


🌐 Web query:

RFC 9110 Authorization authentication scheme case insensitive Bearer

💡 Result:

Under RFC 9110, the authentication scheme identifier used in the HTTP Authorization header is case-insensitive [1][2]. Section 11.1 of RFC 9110 explicitly states that the authentication scheme is identified by a case-insensitive token [1][3]. Consequently, the "Bearer" authentication scheme is case-insensitive [2][4]. Implementations should treat "Bearer", "bearer", "BEARER", and any other casing variations as equivalent [5][4]. While "Bearer" is often used in documentation and examples, it is a matter of convention rather than a strict requirement for the protocol [2]. Many software projects have had to address interoperability issues where parsers incorrectly treated these schemes as case-sensitive; consistent with RFC 9110, these implementations are updated to perform case-insensitive comparisons to ensure compliance [6][5][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
import re

jwt = "header.payload.signature"
inputs = [
    f"Bearer {jwt}",
    f"bearer {jwt}",
    f"BEARER {jwt}",
    f"Bearer  {jwt}",
    jwt,
]

def current(value):
    return value[len("Bearer "):] if value.startswith("Bearer ") else value

def proposed(value):
    return re.sub(r"^Bearer +", "", value, count=1, flags=re.I)

for value in inputs:
    print(f"{value!r}")
    print(f"  current:  {current(value)!r}")
    print(f"  proposed: {proposed(value)!r}")
PY

Repository: corsairdev/corsair

Length of output: 725


Parse the Bearer scheme case-insensitively.

HTTP authentication schemes are case-insensitive. A bearer <JWT> header currently fails JWT validation and tenant matching. Apply the same case-insensitive extraction in packages/monday/webhooks/tenant-matcher.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/monday/webhooks/types.ts` around lines 221 - 223, Update the token
extraction logic around authHeader in types.ts and the corresponding
tenant-matching logic in tenant-matcher.ts to recognize the Bearer scheme
case-insensitively, while preserving the existing raw-token behavior for headers
without that scheme.

@ambikeesshh ambikeesshh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verifier was HMAC against body, monday board webhooks are HS256 JWT
fixed that + bearer casing + fail-closed

lgtm now. you can merge it @devjain32

@devjain32
devjain32 merged commit d56baa0 into corsairdev:main Aug 6, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin Changes inside a plugin package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(monday): webhook handlers skip signature verification

3 participants