Place authentication middleware in the following order:
- Notion webhook auth middleware (X-API-KEY for
/tasks/*
) - Global user auth middleware (for other protected routes)
- Route handlers
- Apply only to routes under /tasks/*
- All /tasks endpoints expect POSTs from Notion webhooks
- Create
/backend/routes/webhookAuthCheck.ts
- Extract X-API-KEY header from incoming requests
- Compare against NOTION_WEBHOOK_SECRET environment variable
- Use constant-time comparison to prevent timing attacks
- Return 401/403 for authentication failures with generic error messages
- Log failed authentication attempts for monitoring
CRITICAL: All webhook routes MUST use POST method:
// ✅ CORRECT - Webhooks are POST requests
app.post("/webhook-endpoint", async (c) => {
// Handle webhook
});
// ❌ WRONG - This won't work with webhooks
app.get("/webhook-endpoint", async (c) => {
// Webhooks can't reach this
});
Why: Notion (and most webhook providers) send POST requests, never GET.
Apply middleware in this EXACT order in main.tsx:
// 1. Webhook auth - ONLY for /tasks/* routes
app.use("/tasks/*", webhookAuth);
// 2. User auth - for ALL routes EXCEPT /tasks/*
app.use("/api/*", authCheck);
app.use("/views/*", authCheck);
app.use("/", authCheck);
// Add root route - now serves the authenticated dashboard
app.get("/", dashboardRoute);
// 3. Mount routes AFTER middleware
app.route("/tasks", taskRoutes);
Important: We've changed the global authentication from app.use("*") to more specific auth route handling.
Before:
app.use("*", authCheck); // Applied to ALL routes including /tasks/*
After:
app.use("/api/*", authCheck); // Applied to API routes
app.use("/views/*", authCheck); // Applied to view routes
app.use("/", authCheck); // Applied to root route
This ensures:
✅ /
(root) still requires user authentication
✅ /api/*
routes still require user authentication
✅ /views/*
routes still require user authentication
✅ /tasks/*
routes only use webhook authentication (no user auth)
Common mistake: Using app.use("*", webhookAuth) - this applies to all routes incorrectly.
- Request to /tasks/* → Check X-API-KEY → If valid, proceed to route handler (skip global auth)
- Request to /tasks/* → Check X-API-KEY → If invalid, return 401 (never reaches global auth)
- Request to other routes → Skip webhook auth → Go to global auth → Then route handler
Test in this exact order:
- Test without header (should fail):
POST /tasks/test Expected: 401 "Authentication required"
- Test with wrong key (should fail):
POST /tasks/test Headers: {"X-API-KEY": "wrong-key"} Expected: 403 "Authentication failed"
- Test with correct key (should succeed):
POST /tasks/test
Headers: {"X-API-KEY": "your-configured-secret-value"}
Expected: 200 (reaches your route handler)
If step 3 returns 404: Your route is probably defined as GET instead of POST.
Add this temporary debug endpoint:
app.get("/debug-webhook", (c) => {
const secret = Deno.env.get("NOTION_WEBHOOK_SECRET");
return c.json({
hasSecret: !!secret,
secretLength: secret?.length || 0,
});
});
Expected response: {"hasSecret":true,"secretLength":>0}
Add this catch all endpoint:
// Catch-all handler for undefined webhook endpoints
// This must be the LAST route defined to catch unmatched paths
app.post("*", (c) => {
const path = c.req.path;
const method = c.req.method;
const returnObj = {
error: "Endpoint not found",
path: path,
method: method,
availableEndpoints: [
"GET /tasks/debug-webhook",
"POST /tasks/test",
"POST /tasks/notion-webhook",
],
};
console.log(returnObj);
return c.json(returnObj, 404);
});
Here's a complete working webhook endpoint:
// backend/routes/tasks/_tasks.routes.ts
import { Hono } from "npm:hono@3.12.12";
const app = new Hono();
app.post("/notion-webhook", async (c) => {
// At this point, webhook auth has already passed
const body = await c.req.json();
console.log("Notion webhook received:", body);
// Process the webhook...
return c.json({ success: true });
});
export default app;
Test with:
POST /tasks/notion-webhook Headers: {"X-API-KEY": "your-configured-secret-value"} Body: {"test": "data"} Expected: 200 {"success": true}
- Webhook middleware should return early (success or failure) without calling next()
- Never pass webhook requests to the global auth middleware
- Maintain complete separation between webhook auth and user auth mechanisms
- Consider rate limiting on /tasks endpoints
- Use generic error messages that don't reveal authentication mechanism details
- /tasks/* → Webhook auth required (Notion POSTs)
- /api/* → User auth required (if applicable)
- /* → Public routes (static files, etc.)
- Added Environment Setup section with clear instructions for setting real values (without revealing actual values)
- Added Route Implementation Requirements emphasizing POST method requirement
- Added exact middleware application code to prevent ordering mistakes
- Added comprehensive testing steps with expected responses (using generic placeholders)
- Added debugging section with tools to troubleshoot issues
- Added complete working example to verify implementation against
- Emphasized critical points with bold text and warning boxes
- Added common mistakes and how to avoid them
These revisions address all the issues we encountered while keeping sensitive values generic and secure.